Skip to content

Commit

Permalink
opt. & new
Browse files Browse the repository at this point in the history
- new: ignore duplicated chats during restoring
- opt.: algo of calc duplication
- opt.: display dups on dialog
  • Loading branch information
lollipopkit committed Mar 22, 2024
1 parent f1b9aaf commit 65d91e8
Show file tree
Hide file tree
Showing 15 changed files with 163 additions and 98 deletions.
8 changes: 4 additions & 4 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PODS:
- audioplayers_darwin (0.0.1):
- Flutter
- countly_flutter (23.12.0):
- countly_flutter (24.1.1):
- Flutter
- DKImagePickerController/Core (4.3.4):
- DKImagePickerController/ImageDataManager
Expand Down Expand Up @@ -109,18 +109,18 @@ EXTERNAL SOURCES:

SPEC CHECKSUMS:
audioplayers_darwin: 877d9a4d06331c5c374595e46e16453ac7eafa40
countly_flutter: 72d4b7ed3921f0dfbf1c0ab8b356e3ecb91bdd74
countly_flutter: 2203707ed19c165b824d030e4dc807ab53437b76
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
icloud_storage: d9ac7a33ced81df08ba7ea1bf3099cc0ee58f60a
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
r_upgrade: 44d715c61914cce3d01ea225abffe894fd51c114
SDWebImage: 96e0c18ef14010b7485210e92fac888587ebb958
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
Expand Down
6 changes: 3 additions & 3 deletions ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 133;
CURRENT_PROJECT_VERSION = 135;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
Expand Down Expand Up @@ -494,7 +494,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 133;
CURRENT_PROJECT_VERSION = 135;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
Expand All @@ -519,7 +519,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 133;
CURRENT_PROJECT_VERSION = 135;
DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
Expand Down
9 changes: 9 additions & 0 deletions lib/core/ext/string.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_chatgpt/data/res/l10n.dart';

extension StringX on String {
/// Format: `#8b2252` or `8b2252`
Expand Down Expand Up @@ -33,3 +34,11 @@ extension StringX on String {
/// Check if a url is a file url (ends with a file extension)
bool get isFileUrl => split('/').lastOrNull?.contains('.') ?? false;
}

extension StringXX on String? {
bool get isNullOrEmpty =>
this == null || this!.isEmpty || this!.trim().isEmpty;

/// - `null` || trim() == '' -> [l10n.untitled]
String get emptyL10n => isNullOrEmpty ? l10n.untitled : this!;
}
8 changes: 0 additions & 8 deletions lib/data/model/app/backup.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,6 @@ class Backup {
await File(await Paths.bak).writeAsString(await backup());
}

/// Merge logic:
/// - Same id:
/// - If [override], restore
/// - If not [override], ignore
/// - New id: restore
/// - Deleted id:
/// - If [override], delete
/// - If not [override], ignore
Future<void> merge({bool force = false}) async {
final curTime = Stores.lastModTime;
final bakTime = lastModTime;
Expand Down
6 changes: 3 additions & 3 deletions lib/data/res/build.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

class Build {
static const String name = "GPTBox";
static const int build = 133;
static const int build = 135;
static const String engine = "3.19.3";
static const String buildAt = "2024-03-21 11:06:50";
static const int modifications = 3;
static const String buildAt = "2024-03-21 14:35:06";
static const int modifications = 5;
}
1 change: 1 addition & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"settings": "Settings",
"share": "Share",
"shareFrom": "Share from",
"skipSameTitle": "Skip chats with titles that are the same as local chats.",
"softWrap": "Soft wrap",
"stt": "stt",
"success": "Success 🏅 ~",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_zh.arb
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"settings": "设置",
"share": "分享",
"shareFrom": "分享自",
"skipSameTitle": "跳过有与本地聊天相同标题的聊天",
"softWrap": "自动换行",
"stt": "语音转文字",
"success": "成功 🏅~",
Expand Down
22 changes: 1 addition & 21 deletions lib/view/page/backup/impl/gpt_next.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,5 @@ void _onTapRestoreGPTNext(BuildContext context) async {
);
});

context.showRoundDialog(
title: l10n.attention,
child: SizedBox(
width: 300,
child: Text(
l10n.sureRestoreFmt('${chats.length} ${l10n.chat}'),
),
),
actions: [
TextButton(
onPressed: () async {
for (final chat in chats) {
Stores.history.put(chat);
}
context.pop();
HomePage.afterRestore();
},
child: Text(l10n.restore),
),
],
);
_askConfirm(context, chats);
}
22 changes: 1 addition & 21 deletions lib/view/page/backup/impl/openai.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,25 +38,5 @@ void _onTapRestoreOpenAI(BuildContext context) async {
);
});

context.showRoundDialog(
title: l10n.attention,
child: SizedBox(
width: 300,
child: Text(
l10n.sureRestoreFmt('${chats.length} ${l10n.chat}'),
),
),
actions: [
TextButton(
onPressed: () async {
for (final chat in chats) {
Stores.history.put(chat);
}
context.pop();
HomePage.afterRestore();
},
child: Text(l10n.restore),
),
],
);
_askConfirm(context, chats);
}
41 changes: 41 additions & 0 deletions lib/view/page/backup/impl/shared.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
part of '../view.dart';

void _askConfirm(BuildContext context, List<ChatHistory> chats) {
var skipSameTitle = true;
context.showRoundDialog(
title: l10n.attention,
child: SizedBox(
width: 300,
child: Column(
children: [
Text(
l10n.sureRestoreFmt('${chats.length} ${l10n.chat}'),
),
StatefulBuilder(
builder: (_, setState) {
return CheckboxListTile(
value: skipSameTitle,
onChanged: (value) => setState(() => skipSameTitle = value!),
title: Text(l10n.skipSameTitle),
);
},
)
],
),
),
actions: [
TextButton(
onPressed: () async {
final keys = Stores.history.box.keys;
for (final chat in chats) {
if (skipSameTitle && keys.contains(chat.id)) continue;
Stores.history.put(chat);
}
context.pop();
HomePage.afterRestore();
},
child: Text(l10n.restore),
),
],
);
}
1 change: 1 addition & 0 deletions lib/view/page/backup/view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ part 'impl/webdav.dart';
part 'impl/icloud.dart';
part 'impl/gpt_next.dart';
part 'impl/openai.dart';
part 'impl/shared.dart';

final _webdavLoading = ValueNotifier(false);

Expand Down
131 changes: 95 additions & 36 deletions lib/view/page/home/ctrl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -185,52 +185,111 @@ Future<void> _onTapImgPick(BuildContext context) async {
_filePicked.value = result;
}

void _removeDuplicateHistory(BuildContext context) async {
final existTitles = <String, String>{}; // {ID: Title}
final rmIds = <String>[];
for (final item in _allHistories.values) {
if (rmIds.contains(item.id)) continue;
final name = item.name;
if (name == null || name.isEmpty) continue;
// For performance, only check the title at first
final titleIncluded = existTitles.values.contains(item.name);
if (titleIncluded) {
// Secondly, check the length of the history
final sameLen = _allHistories[item.id]?.items.length == item.items.length;
if (sameLen) {
// Thirdly, check the content of the history
final sameContent = _allHistories[item.id]
?.items
.map((e) => e.content.first.raw)
.join() ==
item.items.map((e) => e.content.first.raw).join();
if (sameContent) {
rmIds.add(item.id);
Set<String> findAllDuplicateIds(Map<String, ChatHistory> allHistories) {
final existTitles = <String, List<String>>{}; // {"title": ["id"]}
for (final item in allHistories.values) {
final title = item.name ?? '';
existTitles.putIfAbsent(title, () => []).add(item.id);
}

final rmIds = <String>{};
for (final entry in existTitles.entries) {
/// If only one chat with the same title, skip
if (entry.value.length == 1) continue;
final ids = entry.value;

/// If the title is the same, first compare whether the content is the same
/// Collect all assist's reply content
final contentMap = <String, List<String>>{}; // {"id": ["content"]}
final timeMap = <String, int>{}; // {"id": time}
for (final id in ids) {
final history = allHistories[id];
if (history == null) continue;
for (final item in history.items) {
/// Only compare assist's reply which is variety
if (item.role != ChatRole.assist) continue;
final content = item.toMarkdown;
contentMap.putIfAbsent(content, () => []).add(id);
final time = timeMap[id];
if (time == null || item.createdAt.millisecondsSinceEpoch > time) {
timeMap[id] = item.createdAt.millisecondsSinceEpoch;
}
}
} else {
existTitles[item.id] = name;
}

/// Find out the same content
var anyDup = false;
for (var idx = 0; idx < contentMap.length - 1; idx++) {
final contentsA = contentMap.values.elementAt(idx);
final contentsB = contentMap.values.elementAt(idx + 1);
anyDup = contentsA.any((e) => contentsB.contains(e));
if (anyDup) {
break;
}
}

/// If there is no same content, skip
if (!anyDup) continue;

/// If there is same content, delete the old one
var latestTime = timeMap.values.first;
for (final entry in timeMap.entries) {
if (entry.value > latestTime) {
latestTime = entry.value;
}
}

rmIds.addAll(timeMap.entries
.where((e) => e.value != latestTime)
.map((e) => e.key)
.toList());
}
return rmIds;
}

void _removeDuplicateHistory(BuildContext context) async {
final rmIds = await compute(findAllDuplicateIds, _allHistories);
if (rmIds.isEmpty) {
return;
}

final rmCount = rmIds.length;
context.showSnackBarWithAction(
content: l10n.rmDuplicationFmt(rmCount),
action: l10n.delete,
onTap: () {
for (final id in rmIds) {
Stores.history.delete(id);
_allHistories.remove(id);
}
_historyRN.build();
if (!_allHistories.keys.contains(_curChatId)) {
_switchChat();
}
},
final children = <Widget>[Text(l10n.rmDuplicationFmt(rmCount))];
for (int idx = 0; idx < rmCount; idx++) {
final id = rmIds.elementAt(idx);
final item = _allHistories[id];
if (item == null) continue;
children.add(Text(
'${idx + 1}. ${item.items.firstOrNull?.toMarkdown ?? l10n.empty}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: UIs.text12Grey,
));
}
context.showRoundDialog(
title: l10n.attention,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
),
actions: [
TextButton(
onPressed: () {
for (final id in rmIds) {
Stores.history.delete(id);
_allHistories.remove(id);
}
_historyRN.build();
if (!_allHistories.keys.contains(_curChatId)) {
_switchChat();
}
},
child: Text(l10n.delete, style: UIs.textRed),
),
],
);
}

Expand Down
1 change: 1 addition & 0 deletions lib/view/page/home/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import 'package:flutter_chatgpt/core/ext/iterable.dart';
import 'package:flutter_chatgpt/core/ext/list.dart';
import 'package:flutter_chatgpt/core/ext/media_query.dart';
import 'package:flutter_chatgpt/core/ext/num.dart';
import 'package:flutter_chatgpt/core/ext/string.dart';
import 'package:flutter_chatgpt/core/ext/value_notifier.dart';
import 'package:flutter_chatgpt/core/ext/widget.dart';
import 'package:flutter_chatgpt/core/ext/xfile.dart';
Expand Down
2 changes: 1 addition & 1 deletion lib/view/page/home/req.dart
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ Future<void> _genChatTitle(
Create a simple and clear title based on user content.
If the language is Chinese, Japanese or Korean, the title should be within 10 characters;
if it is English, French, German, Latin and other Western languages, the number of title characters should not exceed 23.
The title should be the same as the language entered by the user.''',
The title should be the same as the language entered by the user as below:''',
role: ChatRole.system,
).toOpenAI,
ChatHistoryItem.single(
Expand Down
2 changes: 1 addition & 1 deletion macos/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ SPEC CHECKSUMS:
icloud_storage: 33b05299e26d1391d724da8d62860e702380a1cd
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
Expand Down

0 comments on commit 65d91e8

Please sign in to comment.