♻️ Refactor post draft with drift db

This commit is contained in:
2025-06-24 23:39:09 +08:00
parent 2287995cb4
commit 568d70fffb
11 changed files with 2263 additions and 639 deletions

View File

@ -1,270 +0,0 @@
import 'dart:convert';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:island/pods/config.dart';
part 'compose_storage.g.dart';
const kComposeDraftStoreKey = 'compose_drafts';
const kArticleDraftStoreKey = 'article_drafts';
class ComposeDraft {
final String id;
final String title;
final String description;
final String content;
final List<String> attachmentIds;
final String visibility;
final DateTime lastModified;
ComposeDraft({
required this.id,
required this.title,
required this.description,
required this.content,
required this.attachmentIds,
required this.visibility,
required this.lastModified,
});
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'description': description,
'content': content,
'attachmentIds': attachmentIds,
'visibility': visibility,
'lastModified': lastModified.toIso8601String(),
};
factory ComposeDraft.fromJson(Map<String, dynamic> json) => ComposeDraft(
id: json['id'] as String,
title: json['title'] as String? ?? '',
description: json['description'] as String? ?? '',
content: json['content'] as String? ?? '',
attachmentIds: List<String>.from(json['attachmentIds'] as List? ?? []),
visibility: json['visibility'] as String? ?? 'public',
lastModified: DateTime.parse(json['lastModified'] as String),
);
ComposeDraft copyWith({
String? id,
String? title,
String? description,
String? content,
List<String>? attachmentIds,
String? visibility,
DateTime? lastModified,
}) {
return ComposeDraft(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
content: content ?? this.content,
attachmentIds: attachmentIds ?? this.attachmentIds,
visibility: visibility ?? this.visibility,
lastModified: lastModified ?? this.lastModified,
);
}
bool get isEmpty =>
title.isEmpty &&
description.isEmpty &&
content.isEmpty &&
attachmentIds.isEmpty;
}
class ArticleDraft {
final String id;
final String title;
final String description;
final String content;
final String visibility;
final DateTime lastModified;
ArticleDraft({
required this.id,
required this.title,
required this.description,
required this.content,
required this.visibility,
required this.lastModified,
});
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'description': description,
'content': content,
'visibility': visibility,
'lastModified': lastModified.toIso8601String(),
};
factory ArticleDraft.fromJson(Map<String, dynamic> json) => ArticleDraft(
id: json['id'] as String,
title: json['title'] as String? ?? '',
description: json['description'] as String? ?? '',
content: json['content'] as String? ?? '',
visibility: json['visibility'] as String? ?? 'public',
lastModified: DateTime.parse(json['lastModified'] as String),
);
ArticleDraft copyWith({
String? id,
String? title,
String? description,
String? content,
String? visibility,
DateTime? lastModified,
}) {
return ArticleDraft(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
content: content ?? this.content,
visibility: visibility ?? this.visibility,
lastModified: lastModified ?? this.lastModified,
);
}
bool get isEmpty => title.isEmpty && description.isEmpty && content.isEmpty;
}
@riverpod
class ComposeStorageNotifier extends _$ComposeStorageNotifier {
@override
Map<String, ComposeDraft> build() {
_loadDrafts();
return {};
}
void _loadDrafts() {
final prefs = ref.read(sharedPreferencesProvider);
final draftsJson = prefs.getString(kComposeDraftStoreKey);
if (draftsJson != null) {
try {
final Map<String, dynamic> draftsMap = jsonDecode(draftsJson);
final drafts = <String, ComposeDraft>{};
for (final entry in draftsMap.entries) {
drafts[entry.key] = ComposeDraft.fromJson(entry.value);
}
state = drafts;
} catch (e) {
// If there's an error loading drafts, start with empty state
state = {};
}
}
}
Future<void> _saveDrafts() async {
final prefs = ref.read(sharedPreferencesProvider);
final draftsMap = <String, dynamic>{};
for (final entry in state.entries) {
draftsMap[entry.key] = entry.value.toJson();
}
await prefs.setString(kComposeDraftStoreKey, jsonEncode(draftsMap));
}
Future<void> saveDraft(ComposeDraft draft) async {
if (draft.isEmpty) {
await deleteDraft(draft.id);
return;
}
final updatedDraft = draft.copyWith(lastModified: DateTime.now());
state = {...state, updatedDraft.id: updatedDraft};
await _saveDrafts();
}
Future<void> deleteDraft(String id) async {
final newState = Map<String, ComposeDraft>.from(state);
newState.remove(id);
state = newState;
await _saveDrafts();
}
ComposeDraft? getDraft(String id) {
return state[id];
}
List<ComposeDraft> getAllDrafts() {
final drafts = state.values.toList();
drafts.sort((a, b) => b.lastModified.compareTo(a.lastModified));
return drafts;
}
Future<void> clearAllDrafts() async {
state = {};
final prefs = ref.read(sharedPreferencesProvider);
await prefs.remove(kComposeDraftStoreKey);
}
}
@riverpod
class ArticleStorageNotifier extends _$ArticleStorageNotifier {
@override
Map<String, ArticleDraft> build() {
_loadDrafts();
return {};
}
void _loadDrafts() {
final prefs = ref.read(sharedPreferencesProvider);
final draftsJson = prefs.getString(kArticleDraftStoreKey);
if (draftsJson != null) {
try {
final Map<String, dynamic> draftsMap = jsonDecode(draftsJson);
final drafts = <String, ArticleDraft>{};
for (final entry in draftsMap.entries) {
drafts[entry.key] = ArticleDraft.fromJson(entry.value);
}
state = drafts;
} catch (e) {
// If there's an error loading drafts, start with empty state
state = {};
}
}
}
Future<void> _saveDrafts() async {
final prefs = ref.read(sharedPreferencesProvider);
final draftsMap = <String, dynamic>{};
for (final entry in state.entries) {
draftsMap[entry.key] = entry.value.toJson();
}
await prefs.setString(kArticleDraftStoreKey, jsonEncode(draftsMap));
}
Future<void> saveDraft(ArticleDraft draft) async {
if (draft.isEmpty) {
await deleteDraft(draft.id);
return;
}
final updatedDraft = draft.copyWith(lastModified: DateTime.now());
state = {...state, updatedDraft.id: updatedDraft};
await _saveDrafts();
}
Future<void> deleteDraft(String id) async {
final newState = Map<String, ArticleDraft>.from(state);
newState.remove(id);
state = newState;
await _saveDrafts();
}
ArticleDraft? getDraft(String id) {
return state[id];
}
List<ArticleDraft> getAllDrafts() {
final drafts = state.values.toList();
drafts.sort((a, b) => b.lastModified.compareTo(a.lastModified));
return drafts;
}
Future<void> clearAllDrafts() async {
state = {};
final prefs = ref.read(sharedPreferencesProvider);
await prefs.remove(kArticleDraftStoreKey);
}
}

View File

@ -0,0 +1,341 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:island/database/drift_db.dart';
import 'package:island/pods/database.dart';
part 'compose_storage_db.g.dart';
class ComposeDraftModel {
final String id;
final String title;
final String description;
final String content;
final List<String> attachmentIds;
final String visibility;
final DateTime lastModified;
ComposeDraftModel({
required this.id,
required this.title,
required this.description,
required this.content,
required this.attachmentIds,
required this.visibility,
required this.lastModified,
});
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'description': description,
'content': content,
'attachmentIds': attachmentIds,
'visibility': visibility,
'lastModified': lastModified.toIso8601String(),
};
factory ComposeDraftModel.fromJson(Map<String, dynamic> json) => ComposeDraftModel(
id: json['id'] as String,
title: json['title'] as String? ?? '',
description: json['description'] as String? ?? '',
content: json['content'] as String? ?? '',
attachmentIds: List<String>.from(json['attachmentIds'] as List? ?? []),
visibility: json['visibility'] as String? ?? 'public',
lastModified: DateTime.parse(json['lastModified'] as String),
);
factory ComposeDraftModel.fromDbRow(ComposeDraft row) => ComposeDraftModel(
id: row.id,
title: row.title,
description: row.description,
content: row.content,
attachmentIds: List<String>.from(jsonDecode(row.attachmentIds)),
visibility: row.visibility,
lastModified: row.lastModified,
);
ComposeDraftsCompanion toDbCompanion() => ComposeDraftsCompanion(
id: Value(id),
title: Value(title),
description: Value(description),
content: Value(content),
attachmentIds: Value(jsonEncode(attachmentIds)),
visibility: Value(visibility),
lastModified: Value(lastModified),
);
ComposeDraftModel copyWith({
String? id,
String? title,
String? description,
String? content,
List<String>? attachmentIds,
String? visibility,
DateTime? lastModified,
}) {
return ComposeDraftModel(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
content: content ?? this.content,
attachmentIds: attachmentIds ?? this.attachmentIds,
visibility: visibility ?? this.visibility,
lastModified: lastModified ?? this.lastModified,
);
}
bool get isEmpty =>
title.isEmpty &&
description.isEmpty &&
content.isEmpty &&
attachmentIds.isEmpty;
}
class ArticleDraftModel {
final String id;
final String title;
final String description;
final String content;
final String visibility;
final DateTime lastModified;
ArticleDraftModel({
required this.id,
required this.title,
required this.description,
required this.content,
required this.visibility,
required this.lastModified,
});
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'description': description,
'content': content,
'visibility': visibility,
'lastModified': lastModified.toIso8601String(),
};
factory ArticleDraftModel.fromJson(Map<String, dynamic> json) => ArticleDraftModel(
id: json['id'] as String,
title: json['title'] as String? ?? '',
description: json['description'] as String? ?? '',
content: json['content'] as String? ?? '',
visibility: json['visibility'] as String? ?? 'public',
lastModified: DateTime.parse(json['lastModified'] as String),
);
factory ArticleDraftModel.fromDbRow(ArticleDraft row) => ArticleDraftModel(
id: row.id,
title: row.title,
description: row.description,
content: row.content,
visibility: row.visibility,
lastModified: row.lastModified,
);
ArticleDraftsCompanion toDbCompanion() => ArticleDraftsCompanion(
id: Value(id),
title: Value(title),
description: Value(description),
content: Value(content),
visibility: Value(visibility),
lastModified: Value(lastModified),
);
ArticleDraftModel copyWith({
String? id,
String? title,
String? description,
String? content,
String? visibility,
DateTime? lastModified,
}) {
return ArticleDraftModel(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
content: content ?? this.content,
visibility: visibility ?? this.visibility,
lastModified: lastModified ?? this.lastModified,
);
}
bool get isEmpty => title.isEmpty && description.isEmpty && content.isEmpty;
}
@riverpod
class ComposeStorageNotifier extends _$ComposeStorageNotifier {
@override
Map<String, ComposeDraftModel> build() {
_loadDrafts();
return {};
}
void _loadDrafts() async {
try {
final database = ref.read(databaseProvider);
final dbDrafts = await database.getAllComposeDrafts();
final drafts = <String, ComposeDraftModel>{};
for (final dbDraft in dbDrafts) {
final draft = ComposeDraftModel.fromDbRow(dbDraft);
drafts[draft.id] = draft;
}
state = drafts;
} catch (e) {
// If there's an error loading drafts, start with empty state
state = {};
}
}
Future<void> saveDraft(ComposeDraftModel draft) async {
if (draft.isEmpty) {
await deleteDraft(draft.id);
return;
}
final updatedDraft = draft.copyWith(lastModified: DateTime.now());
state = {...state, updatedDraft.id: updatedDraft};
try {
final database = ref.read(databaseProvider);
await database.saveComposeDraft(updatedDraft.toDbCompanion());
} catch (e) {
// Revert state on error
final newState = Map<String, ComposeDraftModel>.from(state);
newState.remove(updatedDraft.id);
state = newState;
rethrow;
}
}
Future<void> deleteDraft(String id) async {
final oldDraft = state[id];
final newState = Map<String, ComposeDraftModel>.from(state);
newState.remove(id);
state = newState;
try {
final database = ref.read(databaseProvider);
await database.deleteComposeDraft(id);
} catch (e) {
// Revert state on error
if (oldDraft != null) {
state = {...state, id: oldDraft};
}
rethrow;
}
}
ComposeDraftModel? getDraft(String id) {
return state[id];
}
List<ComposeDraftModel> getAllDrafts() {
final drafts = state.values.toList();
drafts.sort((a, b) => b.lastModified.compareTo(a.lastModified));
return drafts;
}
Future<void> clearAllDrafts() async {
state = {};
try {
final database = ref.read(databaseProvider);
await database.clearAllComposeDrafts();
} catch (e) {
// If clearing fails, we might want to reload from database
_loadDrafts();
rethrow;
}
}
}
@riverpod
class ArticleStorageNotifier extends _$ArticleStorageNotifier {
@override
Map<String, ArticleDraftModel> build() {
_loadDrafts();
return {};
}
void _loadDrafts() async {
try {
final database = ref.read(databaseProvider);
final dbDrafts = await database.getAllArticleDrafts();
final drafts = <String, ArticleDraftModel>{};
for (final dbDraft in dbDrafts) {
final draft = ArticleDraftModel.fromDbRow(dbDraft);
drafts[draft.id] = draft;
}
state = drafts;
} catch (e) {
// If there's an error loading drafts, start with empty state
state = {};
}
}
Future<void> saveDraft(ArticleDraftModel draft) async {
if (draft.isEmpty) {
await deleteDraft(draft.id);
return;
}
final updatedDraft = draft.copyWith(lastModified: DateTime.now());
state = {...state, updatedDraft.id: updatedDraft};
try {
final database = ref.read(databaseProvider);
await database.saveArticleDraft(updatedDraft.toDbCompanion());
} catch (e) {
// Revert state on error
final newState = Map<String, ArticleDraftModel>.from(state);
newState.remove(updatedDraft.id);
state = newState;
rethrow;
}
}
Future<void> deleteDraft(String id) async {
final oldDraft = state[id];
final newState = Map<String, ArticleDraftModel>.from(state);
newState.remove(id);
state = newState;
try {
final database = ref.read(databaseProvider);
await database.deleteArticleDraft(id);
} catch (e) {
// Revert state on error
if (oldDraft != null) {
state = {...state, id: oldDraft};
}
rethrow;
}
}
ArticleDraftModel? getDraft(String id) {
return state[id];
}
List<ArticleDraftModel> getAllDrafts() {
final drafts = state.values.toList();
drafts.sort((a, b) => b.lastModified.compareTo(a.lastModified));
return drafts;
}
Future<void> clearAllDrafts() async {
state = {};
try {
final database = ref.read(databaseProvider);
await database.clearAllArticleDrafts();
} catch (e) {
// If clearing fails, we might want to reload from database
_loadDrafts();
rethrow;
}
}
}

View File

@ -1,19 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'compose_storage.dart';
part of 'compose_storage_db.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$composeStorageNotifierHash() =>
r'99c5e4070fa8af2771751064b56ca3251dbda27a';
r'57d3812b8fd430e6144f72708c694ddceea34c17';
/// See also [ComposeStorageNotifier].
@ProviderFor(ComposeStorageNotifier)
final composeStorageNotifierProvider = AutoDisposeNotifierProvider<
ComposeStorageNotifier,
Map<String, ComposeDraft>
Map<String, ComposeDraftModel>
>.internal(
ComposeStorageNotifier.new,
name: r'composeStorageNotifierProvider',
@ -26,15 +26,15 @@ final composeStorageNotifierProvider = AutoDisposeNotifierProvider<
);
typedef _$ComposeStorageNotifier =
AutoDisposeNotifier<Map<String, ComposeDraft>>;
AutoDisposeNotifier<Map<String, ComposeDraftModel>>;
String _$articleStorageNotifierHash() =>
r'4a200878bfe7881fc3afd2164b334e84dc44f338';
r'21ee0f8ee87528bebf8f5f4b0b2892cd8058e230';
/// See also [ArticleStorageNotifier].
@ProviderFor(ArticleStorageNotifier)
final articleStorageNotifierProvider = AutoDisposeNotifierProvider<
ArticleStorageNotifier,
Map<String, ArticleDraft>
Map<String, ArticleDraftModel>
>.internal(
ArticleStorageNotifier.new,
name: r'articleStorageNotifierProvider',
@ -47,6 +47,6 @@ final articleStorageNotifierProvider = AutoDisposeNotifierProvider<
);
typedef _$ArticleStorageNotifier =
AutoDisposeNotifier<Map<String, ArticleDraft>>;
AutoDisposeNotifier<Map<String, ArticleDraftModel>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package