From 9b7a9c12467cedac02c0763d8705e6a27ac24567 Mon Sep 17 00:00:00 2001 From: h-atatsuki Date: Sun, 25 Aug 2024 18:04:43 +0900 Subject: [PATCH] v0.2.0 Portable mode and data sync option --- README.md | 1 + lib/component/drawer.dart | 17 + lib/db/bookmark.dart | 2 + lib/db/bookmark.g.dart | 2 +- lib/db/default_query.dart | 5 + lib/db/default_query.g.dart | 2 +- lib/db/export.dart | 343 +++++++++++ lib/db/export.freezed.dart | 1002 ++++++++++++++++++++++++++++++++ lib/db/export.g.dart | 108 ++++ lib/db/like.dart | 3 + lib/db/like.g.dart | 4 +- lib/download/background.dart | 8 + lib/download/background.g.dart | 2 +- lib/main.dart | 9 + lib/page/sync.dart | 217 +++++++ lib/server/user.dart | 31 + lib/server/user.g.dart | 25 + pubspec.lock | 2 +- pubspec.yaml | 3 +- 19 files changed, 1779 insertions(+), 7 deletions(-) create mode 100644 lib/db/export.dart create mode 100644 lib/db/export.freezed.dart create mode 100644 lib/db/export.g.dart create mode 100644 lib/page/sync.dart create mode 100644 lib/server/user.dart create mode 100644 lib/server/user.g.dart diff --git a/README.md b/README.md index 63e2d48..f032c2c 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Hitomi Search Plus is a lightweight and fast Flutter application that provides a - Per-query bookmarking with automatic updates - Modern UI design with Material Design - Standard features including gallery view and favorites +- `New!` Data synchronization between devices ### Installation diff --git a/lib/component/drawer.dart b/lib/component/drawer.dart index 7f62f16..80b5191 100644 --- a/lib/component/drawer.dart +++ b/lib/component/drawer.dart @@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hitomi_search_plus/page/history.dart'; import 'package:hitomi_search_plus/page/search.dart'; import 'package:hitomi_search_plus/page/settings.dart'; +import 'package:hitomi_search_plus/page/sync.dart'; import 'package:hitomi_search_plus/tools/suger.dart'; import 'package:hitomi_search_plus/tools/url.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -209,6 +210,14 @@ class CustomDrawerPC extends HookConsumerWidget { colorScheme: colorScheme, textTheme: textTheme, ), + _buildExcludedListTile( + context: context, + icon: Icons.sync_alt, + title: 'Sync', + onTap: () => context.push(const DataSyncScreen()), + colorScheme: colorScheme, + textTheme: textTheme, + ), const Divider(), ...links.map((link) => _buildExcludedListTile( context: context, @@ -322,6 +331,14 @@ class CustomDrawer extends HookConsumerWidget { context.push(const HistoryScreenPage()); }, ), + ListTile( + leading: Icon(Icons.sync_alt, color: colorScheme.primary), + title: Text('Sync', style: textTheme.titleMedium), + onTap: () { + Navigator.pop(context); + context.push(const DataSyncScreen()); + }, + ), const Divider(), ...links.map((link) => ListTile( leading: Icon(link.icon, color: colorScheme.secondary), diff --git a/lib/db/bookmark.dart b/lib/db/bookmark.dart index 506c511..cf37a6e 100644 --- a/lib/db/bookmark.dart +++ b/lib/db/bookmark.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:hitomi_search_plus/db/default_query.dart'; +import 'package:hitomi_search_plus/db/export.dart'; import 'package:hitomi_search_plus/db/kv.dart'; import 'package:hitomi_search_plus/main.dart'; import 'package:hitomi_search_plus/server/query.dart'; @@ -272,6 +273,7 @@ class Bookmark extends _$Bookmark { @override List build() { + ref.watch(dataSyncProvider); Future(() async { await load(); await update(); diff --git a/lib/db/bookmark.g.dart b/lib/db/bookmark.g.dart index fd99a05..691a3d9 100644 --- a/lib/db/bookmark.g.dart +++ b/lib/db/bookmark.g.dart @@ -316,7 +316,7 @@ final bookmarkLastUpdateProvider = ); typedef _$BookmarkLastUpdate = Notifier; -String _$bookmarkHash() => r'cea480aa0be6a1cebbfc6ec9f1450802d75e93f8'; +String _$bookmarkHash() => r'd58ababcf70a9ce62370c43878d7fe535b4dbe57'; /// See also [Bookmark]. @ProviderFor(Bookmark) diff --git a/lib/db/default_query.dart b/lib/db/default_query.dart index 7a18dc5..a011cf4 100644 --- a/lib/db/default_query.dart +++ b/lib/db/default_query.dart @@ -170,6 +170,11 @@ class DefaultQueryData extends _$DefaultQueryData { } return buffer.toString(); } + + Future set(DefaultQueryList data) { + state = data; + return _saveToStorage(); + } } @riverpod diff --git a/lib/db/default_query.g.dart b/lib/db/default_query.g.dart index 27f1ae8..c39d2af 100644 --- a/lib/db/default_query.g.dart +++ b/lib/db/default_query.g.dart @@ -58,7 +58,7 @@ Map _$$SelectDefaultQueryImplToJson( // RiverpodGenerator // ************************************************************************** -String _$defaultQueryDataHash() => r'528acf44d23663a85ec09b9890e37211c1ce4987'; +String _$defaultQueryDataHash() => r'94dfdc6174d32656dba76731a4b534d97b8f23eb'; /// See also [DefaultQueryData]. @ProviderFor(DefaultQueryData) diff --git a/lib/db/export.dart b/lib/db/export.dart new file mode 100644 index 0000000..6ef1025 --- /dev/null +++ b/lib/db/export.dart @@ -0,0 +1,343 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hitomi_search_plus/db/default_query.dart'; +import 'package:hitomi_search_plus/main.dart'; +import 'package:hitomi_search_plus/server/user.dart'; +import 'package:http/http.dart' as http; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'export.freezed.dart'; +part 'export.g.dart'; + +@freezed +class BookmarkExportItem with _$BookmarkExportItem { + BookmarkExportItem._(); + factory BookmarkExportItem({ + required int id, + required String query, + required String defaultQuery, + required String? title, + required int addedAt, + }) = _BookmarkExportItem; + + factory BookmarkExportItem.fromJson(Map json) => + _$BookmarkExportItemFromJson(json); +} + +@freezed +class BookmarkExport with _$BookmarkExport { + BookmarkExport._(); + factory BookmarkExport({ + required List items, + }) = _BookmarkExport; + + factory BookmarkExport.fromJson(Map json) => + _$BookmarkExportFromJson(json); +} + +@freezed +class LikeExportItem with _$LikeExportItem { + LikeExportItem._(); + factory LikeExportItem({ + required int id, + required int addedAt, + }) = _LikeExportItem; + + factory LikeExportItem.fromJson(Map json) => + _$LikeExportItemFromJson(json); +} + +@freezed +class ExportData with _$ExportData { + ExportData._(); + factory ExportData({ + required BookmarkExport bookmarks, + required DefaultQueryList defaultQueryList, + required List likes, + }) = _ExportData; + + factory ExportData.fromJson(Map json) => + _$ExportDataFromJson(json); +} + +const url = 'https://hitomi.hiro.red/user'; + +enum SyncState { + none, + working, + error, +} + +@freezed +class DataSyncs with _$DataSyncs { + factory DataSyncs({ + required SyncState upload, + required SyncState download, + }) = _DataSyncs; +} + +@riverpod +class DataSyncState extends _$DataSyncState { + @override + DataSyncs build() { + return DataSyncs( + upload: SyncState.none, + download: SyncState.none, + ); + } + + bool get isWorking => + state.upload == SyncState.working || state.download == SyncState.working; + + void startDownload() { + state = state.copyWith(download: SyncState.working); + } + + void startUpload() { + state = state.copyWith(upload: SyncState.working); + } + + void endDownload() { + state = state.copyWith(download: SyncState.none); + } + + void endUpload() { + state = state.copyWith(upload: SyncState.none); + } + + void errorDownload() { + state = state.copyWith(download: SyncState.error); + } + + void errorUpload() { + state = state.copyWith(upload: SyncState.error); + } +} + +@Riverpod(keepAlive: true) +class DataSync extends _$DataSync { + @override + int build() { + return 0; + } + + Future uploadData() async { + if (ref.read(dataSyncStateProvider.notifier).isWorking) { + return; + } + ref.read(dataSyncStateProvider.notifier).startUpload(); + + final id = await ref.read(userUUIDProvider.future); + try { + final data = await exportData(); + final res = await http.put( + Uri.parse(url), + body: data, + headers: { + 'X-User-Id': id, + 'Content-Type': 'text/plain', + }, + encoding: const Utf8Codec(), + ); + if (res.statusCode != 200) { + throw Exception('Failed to upload: ${res.body}'); + } + } catch (e) { + debugPrint('upload error: $e'); + ref.read(dataSyncStateProvider.notifier).errorUpload(); + return; + } + ref.read(dataSyncStateProvider.notifier).endUpload(); + } + + Future downloadData() async { + if (ref.read(dataSyncStateProvider.notifier).isWorking) { + return; + } + ref.read(dataSyncStateProvider.notifier).startDownload(); + + final id = await ref.read(userUUIDProvider.future); + try { + final response = await http.get( + Uri.parse(url), + headers: { + 'X-User-Id': id, + }, + ); + if (response.statusCode != 200) { + throw Exception('Failed to download: ${response.body}'); + } + await importData(response.body); + } catch (e) { + debugPrint('download error: $e'); + ref.read(dataSyncStateProvider.notifier).errorDownload(); + return; + } + ref.read(dataSyncStateProvider.notifier).endDownload(); + } + + Future exportData() async { + final bookmarks = await db.rawQuery(''' + SELECT id, query, default_query, title, added_at FROM bookmark + '''); + final bookmarkItems = bookmarks + .map((e) => BookmarkExportItem( + id: e['id'] as int, + query: e['query'] as String, + defaultQuery: e['default_query'] as String, + title: e['title'] as String?, + addedAt: e['added_at'] as int, + )) + .toList(); + + final likes = await db.rawQuery(''' + SELECT id, added_at FROM like + '''); + + final likeItems = likes + .map((e) => LikeExportItem( + id: e['id'] as int, + addedAt: e['added_at'] as int, + )) + .toList(); + + final defaultQueryList = ref.read(defaultQueryDataProvider); + + final exportData = ExportData( + bookmarks: BookmarkExport(items: bookmarkItems), + defaultQueryList: defaultQueryList, + likes: likeItems, + ); + + return jsonEncode(exportData.toJson()); + } + + Future importData(String data) async { + final exportData = ExportData.fromJson(jsonDecode(data)); + + await ref + .read(defaultQueryDataProvider.notifier) + .set(exportData.defaultQueryList); + await db.transaction((txn) async { + final List> existingIds = await txn.query( + 'bookmark', + columns: ['id'], + ); + final Set existingIdSet = + existingIds.map((e) => e['id'] as int).toSet(); + for (final item in exportData.bookmarks.items) { + final existingBookmark = await txn.query( + 'bookmark', + where: 'id = ?', + whereArgs: [item.id], + ); + + if (existingBookmark.isNotEmpty) { + await txn.update( + 'bookmark', + { + 'query': item.query, + 'default_query': item.defaultQuery, + 'title': item.title, + 'added_at': item.addedAt, + }, + where: 'id = ?', + whereArgs: [item.id], + ); + } else { + await txn.insert( + 'bookmark', + { + 'id': item.id, + 'query': item.query, + 'default_query': item.defaultQuery, + 'title': item.title, + 'added_at': item.addedAt, + 'success': 0, + 'error': "Data has not been fetched yet", + 'newest_gallery_id': null, + 'count': null, + 'data': null, + }, + ); + } + + existingIdSet.remove(item.id); + } + for (final idToDelete in existingIdSet) { + await txn.delete( + 'bookmark', + where: 'id = ?', + whereArgs: [idToDelete], + ); + } + }); + + await db.transaction((txn) async { + final List> existingIds = await txn.query( + 'like', + columns: ['id'], + ); + final Set existingIdSet = + existingIds.map((e) => e['id'] as int).toSet(); + for (final item in exportData.likes) { + final existingLike = await txn.query( + 'like', + where: 'id = ?', + whereArgs: [item.id], + ); + + int done; + if (existingLike.isNotEmpty) { + done = existingLike.first['done'] as int; + } else { + final taskResult = await txn.query( + 'tasks', + where: 'id = ?', + whereArgs: [item.id], + ); + + if (taskResult.isNotEmpty) { + final task = taskResult.first; + done = (task['total'] as int == task['done'] as int) ? 1 : 0; + } else { + done = 0; + } + } + + if (existingLike.isNotEmpty) { + await txn.update( + 'like', + { + 'added_at': item.addedAt, + 'done': done, + }, + where: 'id = ?', + whereArgs: [item.id], + ); + } else { + await txn.insert( + 'like', + { + 'id': item.id, + 'added_at': item.addedAt, + 'done': done, + }, + ); + } + + existingIdSet.remove(item.id); + } + for (final idToDelete in existingIdSet) { + await txn.delete( + 'like', + where: 'id = ?', + whereArgs: [idToDelete], + ); + } + }); + state = state + 1; + } +} diff --git a/lib/db/export.freezed.dart b/lib/db/export.freezed.dart new file mode 100644 index 0000000..49cc292 --- /dev/null +++ b/lib/db/export.freezed.dart @@ -0,0 +1,1002 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'export.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +BookmarkExportItem _$BookmarkExportItemFromJson(Map json) { + return _BookmarkExportItem.fromJson(json); +} + +/// @nodoc +mixin _$BookmarkExportItem { + int get id => throw _privateConstructorUsedError; + String get query => throw _privateConstructorUsedError; + String get defaultQuery => throw _privateConstructorUsedError; + String? get title => throw _privateConstructorUsedError; + int get addedAt => throw _privateConstructorUsedError; + + /// Serializes this BookmarkExportItem to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of BookmarkExportItem + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $BookmarkExportItemCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $BookmarkExportItemCopyWith<$Res> { + factory $BookmarkExportItemCopyWith( + BookmarkExportItem value, $Res Function(BookmarkExportItem) then) = + _$BookmarkExportItemCopyWithImpl<$Res, BookmarkExportItem>; + @useResult + $Res call( + {int id, String query, String defaultQuery, String? title, int addedAt}); +} + +/// @nodoc +class _$BookmarkExportItemCopyWithImpl<$Res, $Val extends BookmarkExportItem> + implements $BookmarkExportItemCopyWith<$Res> { + _$BookmarkExportItemCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of BookmarkExportItem + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? query = null, + Object? defaultQuery = null, + Object? title = freezed, + Object? addedAt = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + query: null == query + ? _value.query + : query // ignore: cast_nullable_to_non_nullable + as String, + defaultQuery: null == defaultQuery + ? _value.defaultQuery + : defaultQuery // ignore: cast_nullable_to_non_nullable + as String, + title: freezed == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String?, + addedAt: null == addedAt + ? _value.addedAt + : addedAt // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$BookmarkExportItemImplCopyWith<$Res> + implements $BookmarkExportItemCopyWith<$Res> { + factory _$$BookmarkExportItemImplCopyWith(_$BookmarkExportItemImpl value, + $Res Function(_$BookmarkExportItemImpl) then) = + __$$BookmarkExportItemImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int id, String query, String defaultQuery, String? title, int addedAt}); +} + +/// @nodoc +class __$$BookmarkExportItemImplCopyWithImpl<$Res> + extends _$BookmarkExportItemCopyWithImpl<$Res, _$BookmarkExportItemImpl> + implements _$$BookmarkExportItemImplCopyWith<$Res> { + __$$BookmarkExportItemImplCopyWithImpl(_$BookmarkExportItemImpl _value, + $Res Function(_$BookmarkExportItemImpl) _then) + : super(_value, _then); + + /// Create a copy of BookmarkExportItem + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? query = null, + Object? defaultQuery = null, + Object? title = freezed, + Object? addedAt = null, + }) { + return _then(_$BookmarkExportItemImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + query: null == query + ? _value.query + : query // ignore: cast_nullable_to_non_nullable + as String, + defaultQuery: null == defaultQuery + ? _value.defaultQuery + : defaultQuery // ignore: cast_nullable_to_non_nullable + as String, + title: freezed == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String?, + addedAt: null == addedAt + ? _value.addedAt + : addedAt // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$BookmarkExportItemImpl extends _BookmarkExportItem + with DiagnosticableTreeMixin { + _$BookmarkExportItemImpl( + {required this.id, + required this.query, + required this.defaultQuery, + required this.title, + required this.addedAt}) + : super._(); + + factory _$BookmarkExportItemImpl.fromJson(Map json) => + _$$BookmarkExportItemImplFromJson(json); + + @override + final int id; + @override + final String query; + @override + final String defaultQuery; + @override + final String? title; + @override + final int addedAt; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'BookmarkExportItem(id: $id, query: $query, defaultQuery: $defaultQuery, title: $title, addedAt: $addedAt)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'BookmarkExportItem')) + ..add(DiagnosticsProperty('id', id)) + ..add(DiagnosticsProperty('query', query)) + ..add(DiagnosticsProperty('defaultQuery', defaultQuery)) + ..add(DiagnosticsProperty('title', title)) + ..add(DiagnosticsProperty('addedAt', addedAt)); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$BookmarkExportItemImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.query, query) || other.query == query) && + (identical(other.defaultQuery, defaultQuery) || + other.defaultQuery == defaultQuery) && + (identical(other.title, title) || other.title == title) && + (identical(other.addedAt, addedAt) || other.addedAt == addedAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, id, query, defaultQuery, title, addedAt); + + /// Create a copy of BookmarkExportItem + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$BookmarkExportItemImplCopyWith<_$BookmarkExportItemImpl> get copyWith => + __$$BookmarkExportItemImplCopyWithImpl<_$BookmarkExportItemImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$BookmarkExportItemImplToJson( + this, + ); + } +} + +abstract class _BookmarkExportItem extends BookmarkExportItem { + factory _BookmarkExportItem( + {required final int id, + required final String query, + required final String defaultQuery, + required final String? title, + required final int addedAt}) = _$BookmarkExportItemImpl; + _BookmarkExportItem._() : super._(); + + factory _BookmarkExportItem.fromJson(Map json) = + _$BookmarkExportItemImpl.fromJson; + + @override + int get id; + @override + String get query; + @override + String get defaultQuery; + @override + String? get title; + @override + int get addedAt; + + /// Create a copy of BookmarkExportItem + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$BookmarkExportItemImplCopyWith<_$BookmarkExportItemImpl> get copyWith => + throw _privateConstructorUsedError; +} + +BookmarkExport _$BookmarkExportFromJson(Map json) { + return _BookmarkExport.fromJson(json); +} + +/// @nodoc +mixin _$BookmarkExport { + List get items => throw _privateConstructorUsedError; + + /// Serializes this BookmarkExport to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of BookmarkExport + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $BookmarkExportCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $BookmarkExportCopyWith<$Res> { + factory $BookmarkExportCopyWith( + BookmarkExport value, $Res Function(BookmarkExport) then) = + _$BookmarkExportCopyWithImpl<$Res, BookmarkExport>; + @useResult + $Res call({List items}); +} + +/// @nodoc +class _$BookmarkExportCopyWithImpl<$Res, $Val extends BookmarkExport> + implements $BookmarkExportCopyWith<$Res> { + _$BookmarkExportCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of BookmarkExport + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? items = null, + }) { + return _then(_value.copyWith( + items: null == items + ? _value.items + : items // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$BookmarkExportImplCopyWith<$Res> + implements $BookmarkExportCopyWith<$Res> { + factory _$$BookmarkExportImplCopyWith(_$BookmarkExportImpl value, + $Res Function(_$BookmarkExportImpl) then) = + __$$BookmarkExportImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({List items}); +} + +/// @nodoc +class __$$BookmarkExportImplCopyWithImpl<$Res> + extends _$BookmarkExportCopyWithImpl<$Res, _$BookmarkExportImpl> + implements _$$BookmarkExportImplCopyWith<$Res> { + __$$BookmarkExportImplCopyWithImpl( + _$BookmarkExportImpl _value, $Res Function(_$BookmarkExportImpl) _then) + : super(_value, _then); + + /// Create a copy of BookmarkExport + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? items = null, + }) { + return _then(_$BookmarkExportImpl( + items: null == items + ? _value._items + : items // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$BookmarkExportImpl extends _BookmarkExport + with DiagnosticableTreeMixin { + _$BookmarkExportImpl({required final List items}) + : _items = items, + super._(); + + factory _$BookmarkExportImpl.fromJson(Map json) => + _$$BookmarkExportImplFromJson(json); + + final List _items; + @override + List get items { + if (_items is EqualUnmodifiableListView) return _items; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_items); + } + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'BookmarkExport(items: $items)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'BookmarkExport')) + ..add(DiagnosticsProperty('items', items)); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$BookmarkExportImpl && + const DeepCollectionEquality().equals(other._items, _items)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(_items)); + + /// Create a copy of BookmarkExport + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$BookmarkExportImplCopyWith<_$BookmarkExportImpl> get copyWith => + __$$BookmarkExportImplCopyWithImpl<_$BookmarkExportImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$BookmarkExportImplToJson( + this, + ); + } +} + +abstract class _BookmarkExport extends BookmarkExport { + factory _BookmarkExport({required final List items}) = + _$BookmarkExportImpl; + _BookmarkExport._() : super._(); + + factory _BookmarkExport.fromJson(Map json) = + _$BookmarkExportImpl.fromJson; + + @override + List get items; + + /// Create a copy of BookmarkExport + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$BookmarkExportImplCopyWith<_$BookmarkExportImpl> get copyWith => + throw _privateConstructorUsedError; +} + +LikeExportItem _$LikeExportItemFromJson(Map json) { + return _LikeExportItem.fromJson(json); +} + +/// @nodoc +mixin _$LikeExportItem { + int get id => throw _privateConstructorUsedError; + int get addedAt => throw _privateConstructorUsedError; + + /// Serializes this LikeExportItem to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of LikeExportItem + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $LikeExportItemCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LikeExportItemCopyWith<$Res> { + factory $LikeExportItemCopyWith( + LikeExportItem value, $Res Function(LikeExportItem) then) = + _$LikeExportItemCopyWithImpl<$Res, LikeExportItem>; + @useResult + $Res call({int id, int addedAt}); +} + +/// @nodoc +class _$LikeExportItemCopyWithImpl<$Res, $Val extends LikeExportItem> + implements $LikeExportItemCopyWith<$Res> { + _$LikeExportItemCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of LikeExportItem + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? addedAt = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + addedAt: null == addedAt + ? _value.addedAt + : addedAt // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LikeExportItemImplCopyWith<$Res> + implements $LikeExportItemCopyWith<$Res> { + factory _$$LikeExportItemImplCopyWith(_$LikeExportItemImpl value, + $Res Function(_$LikeExportItemImpl) then) = + __$$LikeExportItemImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({int id, int addedAt}); +} + +/// @nodoc +class __$$LikeExportItemImplCopyWithImpl<$Res> + extends _$LikeExportItemCopyWithImpl<$Res, _$LikeExportItemImpl> + implements _$$LikeExportItemImplCopyWith<$Res> { + __$$LikeExportItemImplCopyWithImpl( + _$LikeExportItemImpl _value, $Res Function(_$LikeExportItemImpl) _then) + : super(_value, _then); + + /// Create a copy of LikeExportItem + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? addedAt = null, + }) { + return _then(_$LikeExportItemImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + addedAt: null == addedAt + ? _value.addedAt + : addedAt // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$LikeExportItemImpl extends _LikeExportItem + with DiagnosticableTreeMixin { + _$LikeExportItemImpl({required this.id, required this.addedAt}) : super._(); + + factory _$LikeExportItemImpl.fromJson(Map json) => + _$$LikeExportItemImplFromJson(json); + + @override + final int id; + @override + final int addedAt; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'LikeExportItem(id: $id, addedAt: $addedAt)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'LikeExportItem')) + ..add(DiagnosticsProperty('id', id)) + ..add(DiagnosticsProperty('addedAt', addedAt)); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LikeExportItemImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.addedAt, addedAt) || other.addedAt == addedAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, addedAt); + + /// Create a copy of LikeExportItem + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$LikeExportItemImplCopyWith<_$LikeExportItemImpl> get copyWith => + __$$LikeExportItemImplCopyWithImpl<_$LikeExportItemImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$LikeExportItemImplToJson( + this, + ); + } +} + +abstract class _LikeExportItem extends LikeExportItem { + factory _LikeExportItem({required final int id, required final int addedAt}) = + _$LikeExportItemImpl; + _LikeExportItem._() : super._(); + + factory _LikeExportItem.fromJson(Map json) = + _$LikeExportItemImpl.fromJson; + + @override + int get id; + @override + int get addedAt; + + /// Create a copy of LikeExportItem + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$LikeExportItemImplCopyWith<_$LikeExportItemImpl> get copyWith => + throw _privateConstructorUsedError; +} + +ExportData _$ExportDataFromJson(Map json) { + return _ExportData.fromJson(json); +} + +/// @nodoc +mixin _$ExportData { + BookmarkExport get bookmarks => throw _privateConstructorUsedError; + DefaultQueryList get defaultQueryList => throw _privateConstructorUsedError; + List get likes => throw _privateConstructorUsedError; + + /// Serializes this ExportData to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ExportData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ExportDataCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ExportDataCopyWith<$Res> { + factory $ExportDataCopyWith( + ExportData value, $Res Function(ExportData) then) = + _$ExportDataCopyWithImpl<$Res, ExportData>; + @useResult + $Res call( + {BookmarkExport bookmarks, + DefaultQueryList defaultQueryList, + List likes}); + + $BookmarkExportCopyWith<$Res> get bookmarks; + $DefaultQueryListCopyWith<$Res> get defaultQueryList; +} + +/// @nodoc +class _$ExportDataCopyWithImpl<$Res, $Val extends ExportData> + implements $ExportDataCopyWith<$Res> { + _$ExportDataCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ExportData + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? bookmarks = null, + Object? defaultQueryList = null, + Object? likes = null, + }) { + return _then(_value.copyWith( + bookmarks: null == bookmarks + ? _value.bookmarks + : bookmarks // ignore: cast_nullable_to_non_nullable + as BookmarkExport, + defaultQueryList: null == defaultQueryList + ? _value.defaultQueryList + : defaultQueryList // ignore: cast_nullable_to_non_nullable + as DefaultQueryList, + likes: null == likes + ? _value.likes + : likes // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } + + /// Create a copy of ExportData + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $BookmarkExportCopyWith<$Res> get bookmarks { + return $BookmarkExportCopyWith<$Res>(_value.bookmarks, (value) { + return _then(_value.copyWith(bookmarks: value) as $Val); + }); + } + + /// Create a copy of ExportData + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $DefaultQueryListCopyWith<$Res> get defaultQueryList { + return $DefaultQueryListCopyWith<$Res>(_value.defaultQueryList, (value) { + return _then(_value.copyWith(defaultQueryList: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$ExportDataImplCopyWith<$Res> + implements $ExportDataCopyWith<$Res> { + factory _$$ExportDataImplCopyWith( + _$ExportDataImpl value, $Res Function(_$ExportDataImpl) then) = + __$$ExportDataImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {BookmarkExport bookmarks, + DefaultQueryList defaultQueryList, + List likes}); + + @override + $BookmarkExportCopyWith<$Res> get bookmarks; + @override + $DefaultQueryListCopyWith<$Res> get defaultQueryList; +} + +/// @nodoc +class __$$ExportDataImplCopyWithImpl<$Res> + extends _$ExportDataCopyWithImpl<$Res, _$ExportDataImpl> + implements _$$ExportDataImplCopyWith<$Res> { + __$$ExportDataImplCopyWithImpl( + _$ExportDataImpl _value, $Res Function(_$ExportDataImpl) _then) + : super(_value, _then); + + /// Create a copy of ExportData + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? bookmarks = null, + Object? defaultQueryList = null, + Object? likes = null, + }) { + return _then(_$ExportDataImpl( + bookmarks: null == bookmarks + ? _value.bookmarks + : bookmarks // ignore: cast_nullable_to_non_nullable + as BookmarkExport, + defaultQueryList: null == defaultQueryList + ? _value.defaultQueryList + : defaultQueryList // ignore: cast_nullable_to_non_nullable + as DefaultQueryList, + likes: null == likes + ? _value._likes + : likes // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ExportDataImpl extends _ExportData with DiagnosticableTreeMixin { + _$ExportDataImpl( + {required this.bookmarks, + required this.defaultQueryList, + required final List likes}) + : _likes = likes, + super._(); + + factory _$ExportDataImpl.fromJson(Map json) => + _$$ExportDataImplFromJson(json); + + @override + final BookmarkExport bookmarks; + @override + final DefaultQueryList defaultQueryList; + final List _likes; + @override + List get likes { + if (_likes is EqualUnmodifiableListView) return _likes; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_likes); + } + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'ExportData(bookmarks: $bookmarks, defaultQueryList: $defaultQueryList, likes: $likes)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'ExportData')) + ..add(DiagnosticsProperty('bookmarks', bookmarks)) + ..add(DiagnosticsProperty('defaultQueryList', defaultQueryList)) + ..add(DiagnosticsProperty('likes', likes)); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ExportDataImpl && + (identical(other.bookmarks, bookmarks) || + other.bookmarks == bookmarks) && + (identical(other.defaultQueryList, defaultQueryList) || + other.defaultQueryList == defaultQueryList) && + const DeepCollectionEquality().equals(other._likes, _likes)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, bookmarks, defaultQueryList, + const DeepCollectionEquality().hash(_likes)); + + /// Create a copy of ExportData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ExportDataImplCopyWith<_$ExportDataImpl> get copyWith => + __$$ExportDataImplCopyWithImpl<_$ExportDataImpl>(this, _$identity); + + @override + Map toJson() { + return _$$ExportDataImplToJson( + this, + ); + } +} + +abstract class _ExportData extends ExportData { + factory _ExportData( + {required final BookmarkExport bookmarks, + required final DefaultQueryList defaultQueryList, + required final List likes}) = _$ExportDataImpl; + _ExportData._() : super._(); + + factory _ExportData.fromJson(Map json) = + _$ExportDataImpl.fromJson; + + @override + BookmarkExport get bookmarks; + @override + DefaultQueryList get defaultQueryList; + @override + List get likes; + + /// Create a copy of ExportData + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ExportDataImplCopyWith<_$ExportDataImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$DataSyncs { + SyncState get upload => throw _privateConstructorUsedError; + SyncState get download => throw _privateConstructorUsedError; + + /// Create a copy of DataSyncs + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $DataSyncsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $DataSyncsCopyWith<$Res> { + factory $DataSyncsCopyWith(DataSyncs value, $Res Function(DataSyncs) then) = + _$DataSyncsCopyWithImpl<$Res, DataSyncs>; + @useResult + $Res call({SyncState upload, SyncState download}); +} + +/// @nodoc +class _$DataSyncsCopyWithImpl<$Res, $Val extends DataSyncs> + implements $DataSyncsCopyWith<$Res> { + _$DataSyncsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of DataSyncs + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? upload = null, + Object? download = null, + }) { + return _then(_value.copyWith( + upload: null == upload + ? _value.upload + : upload // ignore: cast_nullable_to_non_nullable + as SyncState, + download: null == download + ? _value.download + : download // ignore: cast_nullable_to_non_nullable + as SyncState, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$DataSyncsImplCopyWith<$Res> + implements $DataSyncsCopyWith<$Res> { + factory _$$DataSyncsImplCopyWith( + _$DataSyncsImpl value, $Res Function(_$DataSyncsImpl) then) = + __$$DataSyncsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({SyncState upload, SyncState download}); +} + +/// @nodoc +class __$$DataSyncsImplCopyWithImpl<$Res> + extends _$DataSyncsCopyWithImpl<$Res, _$DataSyncsImpl> + implements _$$DataSyncsImplCopyWith<$Res> { + __$$DataSyncsImplCopyWithImpl( + _$DataSyncsImpl _value, $Res Function(_$DataSyncsImpl) _then) + : super(_value, _then); + + /// Create a copy of DataSyncs + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? upload = null, + Object? download = null, + }) { + return _then(_$DataSyncsImpl( + upload: null == upload + ? _value.upload + : upload // ignore: cast_nullable_to_non_nullable + as SyncState, + download: null == download + ? _value.download + : download // ignore: cast_nullable_to_non_nullable + as SyncState, + )); + } +} + +/// @nodoc + +class _$DataSyncsImpl with DiagnosticableTreeMixin implements _DataSyncs { + _$DataSyncsImpl({required this.upload, required this.download}); + + @override + final SyncState upload; + @override + final SyncState download; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'DataSyncs(upload: $upload, download: $download)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'DataSyncs')) + ..add(DiagnosticsProperty('upload', upload)) + ..add(DiagnosticsProperty('download', download)); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$DataSyncsImpl && + (identical(other.upload, upload) || other.upload == upload) && + (identical(other.download, download) || + other.download == download)); + } + + @override + int get hashCode => Object.hash(runtimeType, upload, download); + + /// Create a copy of DataSyncs + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$DataSyncsImplCopyWith<_$DataSyncsImpl> get copyWith => + __$$DataSyncsImplCopyWithImpl<_$DataSyncsImpl>(this, _$identity); +} + +abstract class _DataSyncs implements DataSyncs { + factory _DataSyncs( + {required final SyncState upload, + required final SyncState download}) = _$DataSyncsImpl; + + @override + SyncState get upload; + @override + SyncState get download; + + /// Create a copy of DataSyncs + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$DataSyncsImplCopyWith<_$DataSyncsImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/db/export.g.dart b/lib/db/export.g.dart new file mode 100644 index 0000000..f76eb53 --- /dev/null +++ b/lib/db/export.g.dart @@ -0,0 +1,108 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'export.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$BookmarkExportItemImpl _$$BookmarkExportItemImplFromJson( + Map json) => + _$BookmarkExportItemImpl( + id: (json['id'] as num).toInt(), + query: json['query'] as String, + defaultQuery: json['defaultQuery'] as String, + title: json['title'] as String?, + addedAt: (json['addedAt'] as num).toInt(), + ); + +Map _$$BookmarkExportItemImplToJson( + _$BookmarkExportItemImpl instance) => + { + 'id': instance.id, + 'query': instance.query, + 'defaultQuery': instance.defaultQuery, + 'title': instance.title, + 'addedAt': instance.addedAt, + }; + +_$BookmarkExportImpl _$$BookmarkExportImplFromJson(Map json) => + _$BookmarkExportImpl( + items: (json['items'] as List) + .map((e) => BookmarkExportItem.fromJson(e as Map)) + .toList(), + ); + +Map _$$BookmarkExportImplToJson( + _$BookmarkExportImpl instance) => + { + 'items': instance.items, + }; + +_$LikeExportItemImpl _$$LikeExportItemImplFromJson(Map json) => + _$LikeExportItemImpl( + id: (json['id'] as num).toInt(), + addedAt: (json['addedAt'] as num).toInt(), + ); + +Map _$$LikeExportItemImplToJson( + _$LikeExportItemImpl instance) => + { + 'id': instance.id, + 'addedAt': instance.addedAt, + }; + +_$ExportDataImpl _$$ExportDataImplFromJson(Map json) => + _$ExportDataImpl( + bookmarks: + BookmarkExport.fromJson(json['bookmarks'] as Map), + defaultQueryList: DefaultQueryList.fromJson( + json['defaultQueryList'] as Map), + likes: (json['likes'] as List) + .map((e) => LikeExportItem.fromJson(e as Map)) + .toList(), + ); + +Map _$$ExportDataImplToJson(_$ExportDataImpl instance) => + { + 'bookmarks': instance.bookmarks, + 'defaultQueryList': instance.defaultQueryList, + 'likes': instance.likes, + }; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$dataSyncStateHash() => r'b5c91d7716942c91f4392111c203b4874322324e'; + +/// See also [DataSyncState]. +@ProviderFor(DataSyncState) +final dataSyncStateProvider = + AutoDisposeNotifierProvider.internal( + DataSyncState.new, + name: r'dataSyncStateProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$dataSyncStateHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$DataSyncState = AutoDisposeNotifier; +String _$dataSyncHash() => r'441166e68ecbcdc11290befd84bfbc912bc4f91a'; + +/// See also [DataSync]. +@ProviderFor(DataSync) +final dataSyncProvider = NotifierProvider.internal( + DataSync.new, + name: r'dataSyncProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$dataSyncHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$DataSync = Notifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/db/like.dart b/lib/db/like.dart index c55deb3..b65c74f 100644 --- a/lib/db/like.dart +++ b/lib/db/like.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:hitomi_search_plus/db/export.dart'; import 'package:hitomi_search_plus/db/tasks.dart'; import 'package:hitomi_search_plus/main.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -76,6 +77,7 @@ final likeStream = StreamController.broadcast(); class Like extends _$Like { @override bool build(int galleryId) { + ref.watch(dataSyncProvider); Future(() { load(); }); @@ -104,6 +106,7 @@ class Like extends _$Like { class LikeList extends _$LikeList { @override Future build() async { + ref.watch(dataSyncProvider); final result = await db.rawQuery(''' SELECT id FROM like ORDER BY added_at ASC '''); diff --git a/lib/db/like.g.dart b/lib/db/like.g.dart index 801f165..baccc67 100644 --- a/lib/db/like.g.dart +++ b/lib/db/like.g.dart @@ -6,7 +6,7 @@ part of 'like.dart'; // RiverpodGenerator // ************************************************************************** -String _$likeHash() => r'3f715d5b15306264da742c258ee5c34c2bc4abc5'; +String _$likeHash() => r'ba8528f200f29647e88232070ef9e0c72635e974'; /// Copied from Dart SDK class _SystemHash { @@ -164,7 +164,7 @@ class _LikeProviderElement int get galleryId => (origin as LikeProvider).galleryId; } -String _$likeListHash() => r'e05b96c68ffe32796c154cfab11fe086add7a008'; +String _$likeListHash() => r'8a36437bfccc67ef43583b8dec0d9e297ca73194'; /// See also [LikeList]. @ProviderFor(LikeList) diff --git a/lib/download/background.dart b/lib/download/background.dart index 76dcedd..4f586f2 100644 --- a/lib/download/background.dart +++ b/lib/download/background.dart @@ -1,3 +1,4 @@ +import 'package:hitomi_search_plus/db/export.dart'; import 'package:hitomi_search_plus/db/like.dart'; import 'package:hitomi_search_plus/download/download.dart'; import 'package:hitomi_search_plus/download/gallery.dart'; @@ -19,8 +20,15 @@ class BackgroundDownloader extends _$BackgroundDownloader { final listen = likeStream.stream.listen((event) { updateState(event); }); + final listen2 = ref.listen(dataSyncProvider, (v1, v2) { + if (v1 == null) { + return; + } + load(); + }); ref.onDispose(() { listen.cancel(); + listen2.close(); }); return []; } diff --git a/lib/download/background.g.dart b/lib/download/background.g.dart index ef65a04..0dc88c6 100644 --- a/lib/download/background.g.dart +++ b/lib/download/background.g.dart @@ -7,7 +7,7 @@ part of 'background.dart'; // ************************************************************************** String _$backgroundDownloaderHash() => - r'2796005fada69061fcc32f746f952ef4c45eff80'; + r'3e1310b58bf701f8480b2c3468db58da39fdb490'; /// See also [BackgroundDownloader]. @ProviderFor(BackgroundDownloader) diff --git a/lib/main.dart b/lib/main.dart index 2665152..38c7166 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,6 +23,15 @@ Future initPath() async { if (kDebugMode && Platform.isWindows) { appDocDir = Directory('data'); } else { + if (Platform.isWindows) { + // Portable mode + final saveDir = Directory('save'); + if (await saveDir.exists()) { + appDocDir = saveDir; + } else { + appDocDir = await getApplicationDocumentsDirectory(); + } + } appDocDir = await getApplicationDocumentsDirectory(); } db = await initDb(); diff --git a/lib/page/sync.dart b/lib/page/sync.dart new file mode 100644 index 0000000..ecbaa1d --- /dev/null +++ b/lib/page/sync.dart @@ -0,0 +1,217 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hitomi_search_plus/db/export.dart'; +import 'package:hitomi_search_plus/server/user.dart'; +import 'package:hitomi_search_plus/tools/suger.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class DataSyncScreen extends HookConsumerWidget { + const DataSyncScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final uuid = ref.watch(userUUIDProvider); + final status = ref.watch(dataSyncStateProvider); + final theme = Theme.of(context); + + return Scaffold( + appBar: AppBar( + title: const Text('Data Sync'), + ), + body: SingleChildScrollView( + child: Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 600), + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Sync Instructions', + style: theme.textTheme.headlineSmall, + ), + const SizedBox(height: 16), + Text( + '1. On the source device, tap "Upload" to sync your data.\n' + '2. Copy the User ID below.\n' + '3. On the target device, paste the User ID and tap "Download".', + style: theme.textTheme.bodyLarge, + ), + const SizedBox(height: 32), + Text( + 'User ID', + style: theme.textTheme.titleLarge, + ), + const SizedBox(height: 8), + if (uuid.value == null) + const Center(child: CircularProgressIndicator()) + else + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.outline), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData(text: uuid.value!)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Copied to clipboard')), + ); + }, + child: Text( + uuid.value!, + style: theme.textTheme.bodyLarge, + ), + ), + ), + IconButton( + icon: const Icon(Icons.edit), + onPressed: () { + _showUUIDEditDialog(context, ref, uuid.value!); + }, + ), + ], + ), + ), + const SizedBox(height: 48), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SyncButton( + icon: Icons.upload, + label: 'Upload', + status: status.upload, + onPressed: () { + ref.read(dataSyncProvider.notifier).uploadData(); + }, + ), + SyncButton( + icon: Icons.download, + label: 'Download', + status: status.download, + onPressed: () { + ref.read(dataSyncProvider.notifier).downloadData(); + }, + ), + ], + ), + ], + ), + ), + ), + ), + ); + } + + void _showUUIDEditDialog( + BuildContext context, WidgetRef ref, String currentUUID) { + final controller = TextEditingController(text: currentUUID); + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Edit UUID'), + content: TextField( + controller: controller, + decoration: const InputDecoration( + hintText: 'Enter new UUID', + ), + ), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('Save'), + onPressed: () { + final newUUID = controller.text.trim(); + if (_isValidUUID(newUUID)) { + ref.read(userUUIDProvider.notifier).set(newUUID); + Navigator.of(context).pop(); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Invalid UUID format')), + ); + } + }, + ), + ], + ); + }, + ); + } + + bool _isValidUUID(String uuid) { + final uuidRegex = RegExp( + r'^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$', + caseSensitive: false, + ); + return uuidRegex.hasMatch(uuid); + } +} + +class SyncButton extends StatelessWidget { + final IconData icon; + final String label; + final SyncState status; + final VoidCallback onPressed; + + const SyncButton({ + super.key, + required this.icon, + required this.label, + required this.status, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Column( + children: [ + ElevatedButton( + onPressed: status == SyncState.working ? null : onPressed, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + padding: const EdgeInsets.all(20), + backgroundColor: _getColor(context), + ), + child: Icon(icon, size: 30, color: theme.colorScheme.onPrimary), + ), + const SizedBox(height: 8), + Text(label, style: theme.textTheme.titleMedium), + const SizedBox(height: 4), + Text(_getStatusText(), style: theme.textTheme.bodySmall), + ], + ); + } + + Color _getColor(BuildContext context) { + switch (status) { + case SyncState.none: + return context.theme.colorScheme.secondary; + case SyncState.working: + return context.theme.colorScheme.primary; + case SyncState.error: + return context.theme.colorScheme.error; + } + } + + String _getStatusText() { + switch (status) { + case SyncState.none: + return 'Idle'; + case SyncState.working: + return 'Working'; + case SyncState.error: + return 'Error'; + } + } +} \ No newline at end of file diff --git a/lib/server/user.dart b/lib/server/user.dart new file mode 100644 index 0000000..b6a299a --- /dev/null +++ b/lib/server/user.dart @@ -0,0 +1,31 @@ +import 'package:hitomi_search_plus/db/kv.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:uuid/uuid.dart'; + +part 'user.g.dart'; + +const key = 'user_uuid'; + +@riverpod +class UserUUID extends _$UserUUID { + @override + Future build() async { + final res = await getKV(key); + + if (res == null) { + final uuid = const Uuid().v4(); + await setKV(key, uuid); + return uuid; + } + + return res; + } + + Future set(String data) async { + if (data.isEmpty) { + return; + } + state = AsyncData(data); + await setKV(key, data); + } +} \ No newline at end of file diff --git a/lib/server/user.g.dart b/lib/server/user.g.dart new file mode 100644 index 0000000..462ed39 --- /dev/null +++ b/lib/server/user.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$userUUIDHash() => r'788a73fbced7fe75060e638d9257035f55785632'; + +/// See also [UserUUID]. +@ProviderFor(UserUUID) +final userUUIDProvider = + AutoDisposeAsyncNotifierProvider.internal( + UserUUID.new, + name: r'userUUIDProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$userUUIDHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$UserUUID = AutoDisposeAsyncNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/pubspec.lock b/pubspec.lock index c63efbe..e3ec91a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1009,7 +1009,7 @@ packages: source: hosted version: "3.1.2" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" diff --git a/pubspec.yaml b/pubspec.yaml index 4f68e51..e1a8f33 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.1.5+1 +version: 0.2.0+1 environment: sdk: ^3.5.0 @@ -51,6 +51,7 @@ dependencies: sqlite3_flutter_libs: ^0.5.24 url_launcher: ^6.3.0 fullscreen_window: ^1.0.4 + uuid: ^4.4.2 dev_dependencies: flutter_test: