Skip to content

Commit

Permalink
new: uni-link
Browse files Browse the repository at this point in the history
  • Loading branch information
lollipopkit committed Feb 2, 2024
1 parent 9a94028 commit a5eee9a
Show file tree
Hide file tree
Showing 20 changed files with 356 additions and 66 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ This app is now under development, some features may be missing / not available.
## 🪄 Features
- Restore from [ChatGPT Next Web](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web)
- Share chat by picture.
- Render code block / latex formula.
- Uni-Link, eg: `lk-gptbox://chat/new?msg=hello` (no Linux)
- All platforms support.
- Sync with WebDAV / iCloud.
- Render code block / latex formula.


## 🏙️ Screenshots
Expand Down
3 changes: 2 additions & 1 deletion README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@

## 🪄 特性
-[ChatGPT Next Web](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) 恢复
- 与 WebDAV / iCloud 同步
- 以图片形式分享聊天
- Uni-Link,例如:`lk-gptbox://chat/new?msg=你好` (Linux 除外)
- 与 WebDAV / iCloud 同步
- 全平台支持
- 渲染 代码块 / LaTeX 公式

Expand Down
16 changes: 9 additions & 7 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@
- [x] File
- [x] Clipboard
- [x] WebDAV
- [ ] URL Scheme
- [ ] Config: `gptbox://config/set?openAiUrl=&openAiToken=&openAiModel=`
- [ ] Chat
- [ ] New: `gptbox://chat/new?msg=`
- [ ] Open: `gptbox://chat/open?chatId=`
- [ ] Search: `gptbox://chat/search?keyword=`
- [ ] Share: `gptbox://chat/share?chatId=`
- [x] URL Scheme
- [x] Config: `lk-gptbox://config/set?openAiUrl=&openAiKey=&openAiModel=&chatId=`
- If chatId is not provided, it will config global settings
- If chatId not exists, it will create a new chat
- [x] Chat
- [x] New: `lk-gptbox://chat/new?msg=`
- [x] Open: `lk-gptbox://chat/open?chatId=`
- [x] Search: `lk-gptbox://chat/search?keyword=`
- [x] Share: `lk-gptbox://chat/share?chatId=`
9 changes: 9 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
<data
android:scheme="lk-gptbox"
android:host="chat" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
Expand Down
13 changes: 13 additions & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,18 @@
</array>
</dict>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>tech.lolli.gptbox</string>
<key>CFBundleURLSchemes</key>
<array>
<string>lk-gptbox</string>
</array>
</dict>
</array>
</dict>
</plist>
10 changes: 10 additions & 0 deletions lib/core/ext/list.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
extension ListX<T> on List<T> {
T? firstWhereOrNull(bool Function(T) test) {
for (final e in this) {
if (test(e)) {
return e;
}
}
return null;
}
}
9 changes: 5 additions & 4 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
"autoRmDupChat": "Auto remove duplicate chats",
"autoScrollBottom": "Auto scroll to bottom",
"backup": "Backup",
"backupFailed": "Backup failed: {reason}",
"backupRestorationFailed": "Failed to restore backup: {reason}",
"backupRestorationSuccessful": "Successfully restored backup",
"backupSuccessful": "Backup completed successfully",
"backupTip": "Please keep backup files private and safe!",
"backupSuccessful": "Backup completed successfully",
"backupFailed": "Backup failed: {reason}",
"backupRestorationSuccessful": "Successfully restored backup",
"backupRestorationFailed": "Failed to restore backup: {reason}",
"cancel": "Cancel",
"changeModelTip": "Different keys may be able to access different lists of models, so if you don't understand the mechanism and get an error, it is recommended to reset the model.",
"chat": "Chat",
Expand Down Expand Up @@ -47,6 +47,7 @@
"hoursAgo": "{hours} hours ago",
"ignoreTip": "Ignore tips",
"initChatHelp": "### 📖 Tip\n- Overscroll on the chat page to switch chat history.\n- Long press code block to copy\n- The left drawer settings are global settings, the settings above the input box below are settings for the current conversation\n- Swipe right to enter the left side of the history page, you can modify the title of the chat, delete the chat\n\n### 🔍 Help\n- If you have found a bug, please use [Github Issue](https://github.com/lollipopkit/flutter_gpt_box/issues)",
"invalidLinkFmt": "Invalid link: {uri}",
"justNow": "Just now",
"lang": "Language",
"license": "License",
Expand Down
9 changes: 5 additions & 4 deletions lib/l10n/app_zh.arb
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
"autoRmDupChat": "自动删除重复聊天",
"autoScrollBottom": "自动滚动到底部",
"backup": "备份",
"backupFailed": "备份失败:{reason}",
"backupRestorationFailed": "未能恢复备份:{reason}",
"backupRestorationSuccessful": "成功恢复备份",
"backupSuccessful": "备份成功",
"backupTip": "请保证备份文件私密且安全!",
"backupSuccessful": "备份成功",
"backupFailed": "备份失败:{reason}",
"backupRestorationSuccessful": "成功恢复备份",
"backupRestorationFailed": "未能恢复备份:{reason}",
"cancel": "取消",
"changeModelTip": "不同密钥可能能访问的模型列表不同,如果不了解机制并且出现错误,建议重新设置模型。",
"chat": "聊天",
Expand Down Expand Up @@ -47,6 +47,7 @@
"hoursAgo": "{hours} 小时前",
"ignoreTip": "忽略提示",
"initChatHelp": "### 📖 提示\n- 在聊天界面过度滑动(overscroll)可以快捷切换聊天历史记录\n- 长按代码块来复制\n- 左侧抽屉设置为全局设置,下方的输入框上方的设置为当前对话的设置\n- 向右滑动可以进入左侧的历史聊天记录页面,可以修改聊天标题、删除聊天\n\n### 🔍 帮助\n- 如果 GPT Box 有 bug,请使用 [Github Issue](https://github.com/lollipopkit/flutter_gpt_box/issues)\n- QQ群 762870488",
"invalidLinkFmt": "未知链接:{uri}",
"justNow": "刚刚",
"lang": "语言",
"license": "许可证",
Expand Down
5 changes: 5 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'package:flutter_chatgpt/data/store/all.dart';
import 'package:flutter_chatgpt/view/widget/appbar.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:logging/logging.dart';
import 'package:uni_links_desktop/uni_links_desktop.dart';
import 'package:window_manager/window_manager.dart';

Future<void> main() async {
Expand Down Expand Up @@ -61,6 +62,10 @@ Future<void> _initApp() async {
OpenAICfg.apply();

SyncService.sync(force: true);

if (isWindows) {
registerProtocol('lk-gptbox');
}
}

Future<void> _initDb() async {
Expand Down
28 changes: 23 additions & 5 deletions lib/view/page/home/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:screenshot/screenshot.dart';
import 'package:share_plus/share_plus.dart';
import 'package:uni_links/uni_links.dart';

part 'setting.dart';
part 'chat.dart';
Expand All @@ -51,6 +52,7 @@ part 'enum.dart';
part 'search.dart';
part 'appbar.dart';
part 'input.dart';
part 'uni_link.dart';

class HomePage extends StatefulWidget {
const HomePage({super.key});
Expand All @@ -72,6 +74,7 @@ class _HomePageState extends State<HomePage>
(_) => _timeRN.rebuild(),
);
_historyScrollCtrl.addListener(_locateHistoryListener);
_initUniLinks();
}

@override
Expand Down Expand Up @@ -182,16 +185,31 @@ class _HomePageState extends State<HomePage>

@override
FutureOr<void> afterFirstLayout(BuildContext context) {
// Keep this here.
// - If there is not chat history, [_switchChat] will create one
// - If the init help haven't shown, [_switchChat] will show it
// - Init help uses [l10n] to gen msg, so [l10n] must be ready
// - [l10n] is ready after first layout
/// Keep this here.
/// - If there is not chat history, [_switchChat] will create one
/// - If the init help haven't shown, [_switchChat] will show it
/// - Init help uses [l10n] to gen msg, so [l10n] must be ready
/// - [l10n] is ready after first layout
_switchChat();
_removeDuplicateHistory(context);

if (Stores.setting.autoCheckUpdate.fetch()) {
AppUpdateIface.doUpdate(context);
}
}

void _initUniLinks() async {
uriLinkStream.listen((Uri? uri) {
if (uri == null) return;
if (!mounted) return;
AppLink.handle(context, uri);
}, onError: (err) {
final msg = l10n.invalidLinkFmt(err);
Loggers.app.warning(msg);
context.showRoundDialog(
title: l10n.attention,
child: Text(msg),
);
});
}
}
4 changes: 3 additions & 1 deletion lib/view/page/home/search.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
part of 'home.dart';

class ChatSearchDelegate extends SearchDelegate<ChatHistory> {
ChatSearchDelegate();
ChatSearchDelegate({String? initKeyword}) {
query = initKeyword ?? '';
}

@override
List<Widget> buildActions(BuildContext context) {
Expand Down
160 changes: 160 additions & 0 deletions lib/view/page/home/uni_link.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
part of 'home.dart';

abstract final class AppLink {
static const scheme = 'lk-gptbox';

static final List<AppLinkHost> hosts = <AppLinkHost>[
AppLinkHostChat._(),
AppLinkHostConfig._(),
];

/// example: lk-gptbox://chat/ACTION?PARAM=PARAM_VALUE
static bool? handle(BuildContext context, Uri uri) {
if (uri.scheme != scheme) {
return false;
}
final handlers = hosts.where((e) => e.host == uri.host);
if (handlers.isEmpty) {
return null;
}
for (final handler in handlers) {
if (handler.handler(context, uri.path, uri.queryParameters)) {
return true;
}
}
return false;
}
}

abstract final class AppLinkHost {
String get host => throw UnimplementedError();

/// Return [true] to block other handlers, [false] to fallback to next handler
bool handler(BuildContext context, String path, Map<String, String> params) {
return false;
}
}

final class AppLinkHostChat extends AppLinkHost {
AppLinkHostChat._();

@override
String get host => 'chat';

@override
bool handler(BuildContext context, String path, Map<String, String> params) {
switch (path) {
case '/new':
final msg = params['msg'];
final chat = _newChat();
_switchChat(chat.id);
if (msg != null) {
_inputCtrl.text = msg;
_onSend(chat.id, context);
}
return true;
case '/open':
final chatId = params['chatId'];
if (chatId != null) {
_switchChat(chatId);
return true;
}
final msg = l10n.invalidLinkFmt('${l10n.empty} chatId');
context.showSnackBar(msg);
Loggers.app.warning(msg);
return true;
case '/search':
final query = params['keyword'];
showSearch(
context: context, delegate: ChatSearchDelegate(initKeyword: query));
return true;
case '/share':
final chatId = params['chatId'];
if (chatId != null) {
final chat = _allHistories[chatId];
if (chat != null) {
_switchChat(chat.id);
_onShareChat(context);
return true;
}
}
final msg = l10n.invalidLinkFmt('${l10n.empty} chatId');
context.showSnackBar(msg);
Loggers.app.warning(msg);
return true;
default:
final msg = l10n.invalidLinkFmt(path);
context.showSnackBar(msg);
Loggers.app.warning(msg);
return false;
}
}
}

final class AppLinkHostConfig extends AppLinkHost {
AppLinkHostConfig._();

@override
String get host => 'config';

@override
bool handler(BuildContext context, String path, Map<String, String> params) {
switch (path) {
case '/open':
Routes.setting.go(context);
return true;
case '/set':
final openAiUrl = params['openAiUrl'];
final openAiKey = params['openAiKey'];
final openAiModel = params['openAiModel'];
if (openAiKey == null && openAiUrl == null && openAiModel == null) {
final msg = l10n.invalidLinkFmt('${l10n.empty} config');
context.showSnackBar(msg);
Loggers.app.warning(msg);
return true;
}
final chatId = params['chatId'];
if (chatId != null) {
final chat = _allHistories[chatId];
if (chat == null) {
final msg = l10n.invalidLinkFmt('no chatId($chatId)');
context.showSnackBar(msg);
Loggers.app.warning(msg);
return true;
}
var cfg = chat.config;
if (cfg != null) {
if (openAiKey != null) {
cfg = cfg.copyWith(key: openAiKey);
}
if (openAiUrl != null) {
cfg = cfg.copyWith(url: openAiUrl);
}
if (openAiModel != null) {
cfg = cfg.copyWith(model: openAiModel);
}
chat.config = cfg;
_storeChat(chatId, context);
}
} else {
if (openAiKey != null) {
Stores.setting.openaiApiKey.put(openAiKey);
OpenAICfg.key = openAiKey;
}
if (openAiUrl != null) {
Stores.setting.openaiApiUrl.put(openAiUrl);
OpenAICfg.url = openAiUrl;
}
if (openAiModel != null) {
Stores.setting.openaiModel.put(openAiModel);
}
}
return true;
default:
final msg = l10n.invalidLinkFmt(path);
context.showSnackBar(msg);
Loggers.app.warning(msg);
return false;
}
}
}
Loading

0 comments on commit a5eee9a

Please sign in to comment.