add quick action and desktop, bookmark add edit dialog

This commit is contained in:
DASHU
2024-12-05 19:01:06 +08:00
parent 7e36540e1e
commit c50508c7c8
17 changed files with 588 additions and 25 deletions

BIN
assets/imgs/browser.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,201 @@
import 'package:flutter/material.dart';
import 'package:nostr_sdk/utils/platform_util.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import 'package:nowser/data/bookmark.dart';
import 'package:nowser/data/bookmark_db.dart';
import 'package:quick_actions/quick_actions.dart';
import '../const/base.dart';
import '../main.dart';
import '../util/router_util.dart';
import '../util/table_mode_util.dart';
import '../util/theme_util.dart';
class BookmarkEditDialog extends StatefulWidget {
Bookmark bookmark;
BookmarkEditDialog(this.bookmark);
static Future<void> show(BuildContext context, Bookmark bookmark) async {
await showDialog<String>(
context: context,
builder: (_context) {
return BookmarkEditDialog(bookmark);
},
);
}
@override
State<StatefulWidget> createState() {
return _BookmarkEditDialog();
}
}
class _BookmarkEditDialog extends State<BookmarkEditDialog> {
TextEditingController nameTextController = TextEditingController();
TextEditingController urlTextController = TextEditingController();
bool addedToIndex = false;
bool addedToQa = false;
@override
void initState() {
super.initState();
if (StringUtil.isNotBlank(widget.bookmark.title)) {
nameTextController.text = widget.bookmark.title!;
}
if (StringUtil.isNotBlank(widget.bookmark.url)) {
urlTextController.text = widget.bookmark.url!;
}
}
@override
Widget build(BuildContext context) {
var themeData = Theme.of(context);
List<Widget> list = [];
list.add(Container(
margin: EdgeInsets.only(
bottom: Base.BASE_PADDING,
),
child: Text(
"Add bookmark",
style: TextStyle(
fontSize: themeData.textTheme.bodyLarge!.fontSize,
fontWeight: FontWeight.bold,
),
),
));
list.add(Container(
child: TextField(
controller: nameTextController,
decoration: InputDecoration(
labelText: "Name",
),
),
));
list.add(Container(
child: TextField(
controller: urlTextController,
decoration: InputDecoration(
labelText: "Url",
),
),
));
list.add(Container(
margin: EdgeInsets.only(top: Base.BASE_PADDING_HALF),
child: Row(
children: [
Text("Add to index"),
Expanded(
child: Checkbox(
value: addedToIndex,
onChanged: (v) {
if (v != null) {
setState(() {
addedToIndex = v;
});
}
},
),
)
],
),
));
list.add(Container(
child: Row(
children: [
Text("Add to quick action"),
Expanded(
child: Checkbox(
value: addedToQa,
onChanged: (v) {
if (v != null) {
setState(() {
addedToQa = v;
});
}
},
),
)
],
),
));
list.add(Container(
margin: EdgeInsets.only(
top: Base.BASE_PADDING * 2,
bottom: Base.BASE_PADDING,
),
width: double.infinity,
child: FilledButton(onPressed: confirm, child: Text("Confirm")),
));
Widget main = Container(
child: Column(
mainAxisSize: MainAxisSize.min,
children: list,
),
);
if (PlatformUtil.isPC() || TableModeUtil.isTableMode()) {
main = Container(
width: mediaDataCache.size.width / 2,
child: main,
);
}
return Dialog(
child: Container(
padding: EdgeInsets.all(Base.BASE_PADDING * 2),
child: main,
),
);
}
Future<void> confirm() async {
var title = nameTextController.text;
var url = urlTextController.text;
var bookmark = Bookmark(
title: title,
url: url,
id: widget.bookmark.id,
favicon: widget.bookmark.favicon,
weight: widget.bookmark.weight,
createdAt: widget.bookmark.createdAt,
addedToIndex: addedToIndex ? 1 : -1,
addedToQa: addedToQa ? 1 : -1,
);
if (bookmark.id == null) {
await BookmarkDB.insert(bookmark);
} else {
await BookmarkDB.update(bookmark);
}
try {
var allQas = await BookmarkDB.allQas();
List<ShortcutItem> qas = [];
for (var bk in allQas) {
if (StringUtil.isBlank(bk.title) || StringUtil.isBlank(bk.url)) {
continue;
}
qas.add(ShortcutItem(
type: bk.url!, localizedTitle: bk.title!, icon: 'ic_launcher'));
quickActions.setShortcutItems(qas);
}
} catch (e) {
print(e);
}
RouterUtil.back(context);
}
}

View File

@@ -5,6 +5,7 @@ class Bookmark {
String? favicon;
int? weight;
int? addedToIndex;
int? addedToQa;
int? createdAt;
Bookmark(
@@ -14,6 +15,7 @@ class Bookmark {
this.favicon,
this.weight,
this.addedToIndex,
this.addedToQa,
this.createdAt});
Bookmark.fromJson(Map<String, dynamic> json) {
@@ -23,6 +25,7 @@ class Bookmark {
favicon = json['favicon'];
weight = json['weight'];
addedToIndex = json['added_to_index'];
addedToQa = json['added_to_qa'];
createdAt = json['created_at'];
}
@@ -34,6 +37,7 @@ class Bookmark {
data['favicon'] = this.favicon;
data['weight'] = this.weight;
data['added_to_index'] = this.addedToIndex;
data['added_to_qa'] = this.addedToQa;
data['created_at'] = this.createdAt;
return data;
}

View File

@@ -28,6 +28,20 @@ class BookmarkDB {
return objs;
}
static Future<List<Bookmark>> allQas({DatabaseExecutor? db}) async {
List<Bookmark> objs = [];
List<Object?>? arguments = [];
db = await DB.getDB(db);
var sql =
"select * from bookmark where added_to_qa = 1 order by created_at desc";
List<Map<String, dynamic>> list = await db.rawQuery(sql, arguments);
for (var i = 0; i < list.length; i++) {
var json = list[i];
objs.add(Bookmark.fromJson(json));
}
return objs;
}
static Future<void> deleteByIds(List<int> ids, {DatabaseExecutor? db}) async {
var sql = "delete from bookmark where id in(";
for (var id in ids) {
@@ -39,4 +53,9 @@ class BookmarkDB {
db = await DB.getDB(db);
await db.execute(sql, ids);
}
static Future update(Bookmark o, {DatabaseExecutor? db}) async {
db = await DB.getDB(db);
await db.update("bookmark", o.toJson(), where: "id = ?", whereArgs: [o.id]);
}
}

View File

@@ -6,7 +6,7 @@ import 'package:path/path.dart';
import 'package:process_run/shell_run.dart';
class DB {
static const _VERSION = 1;
static const _VERSION = 2;
static const _dbName = "nowser.db";
@@ -21,14 +21,14 @@ class DB {
}
try {
_database =
await openDatabase(path, version: _VERSION, onCreate: _onCreate);
_database = await openDatabase(path,
version: _VERSION, onCreate: _onCreate, onUpgrade: _onUpgrade);
} catch (e) {
if (Platform.isLinux) {
// maybe it need install sqlite first, but this command need run by root.
await run('sudo apt-get -y install libsqlite3-0 libsqlite3-dev');
_database =
await openDatabase(path, version: _VERSION, onCreate: _onCreate);
_database = await openDatabase(path,
version: _VERSION, onCreate: _onCreate, onUpgrade: _onUpgrade);
}
}
}
@@ -50,11 +50,19 @@ class DB {
db.execute("create index zap_log_index on zap_log (app_id);");
db.execute(
"create table bookmark(id integer not null constraint bookmark_pk primary key autoincrement,title text,url text not null,favicon text,weight integer,added_to_index integer,created_at integer);");
"create table bookmark(id integer not null constraint bookmark_pk primary key autoincrement,title text,url text not null,favicon text,weight integer,added_to_index integer, added_to_qa integer,created_at integer);");
db.execute(
"create table browser_history(id integer not null constraint browser_history_pk primary key autoincrement,title text,url text not null,favicon text,created_at integer);");
}
static Future<void> _onUpgrade(
Database db, int oldVersion, int newVersion) async {
if (oldVersion == 1 && newVersion == 2) {
db.execute(
"alter table bookmark add added_to_qa integer after added_to_index");
}
}
static Future<Database> getCurrentDatabase() async {
if (_database == null) {
await init();

View File

@@ -28,6 +28,7 @@ import 'package:nowser/router/me/me_router.dart';
import 'package:nowser/router/web_tabs_select/web_tabs_select_router.dart';
import 'package:nowser/router/web_url_input/web_url_input_router.dart';
import 'package:provider/provider.dart';
import 'package:quick_actions/quick_actions.dart';
import 'package:receive_intent/receive_intent.dart' as receiveIntent;
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:window_manager/window_manager.dart';
@@ -41,6 +42,7 @@ import 'provider/data_util.dart';
import 'provider/remote_signing_provider.dart';
import 'provider/setting_provider.dart';
import 'util/colors_util.dart';
import 'util/media_data_cache.dart';
late WebProvider webProvider;
@@ -58,6 +60,10 @@ late RootIsolateToken rootIsolateToken;
late BuildInRelayProvider buildInRelayProvider;
const QuickActions quickActions = QuickActions();
late MediaDataCache mediaDataCache;
Future<void> main() async {
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
rootIsolateToken = RootIsolateToken.instance!;

View File

@@ -38,15 +38,26 @@ mixin AndroidSignerMixin on PermissionCheckMixin {
static const String PREFIX = "nostrsigner:";
Future<void> handleInitialIntent(BuildContext context) async {
final intent = await receiveIntent.ReceiveIntent.getInitialIntent();
if (intent != null) {
// log(intent.toString());
// log("from ${intent.fromPackageName}");
// log("action ${intent.action}");
// log("data ${intent.data}");
// log("categories ${intent.categories}");
// log("extra ${intent.extra}");
var intent = await getInitialIntent();
await dohandleInitialIntent(context, intent);
}
Future<receiveIntent.Intent?> getInitialIntent() async {
var intent = await receiveIntent.ReceiveIntent.getInitialIntent();
if (intent != null) {
log(intent.toString());
log("from ${intent.fromPackageName}");
log("action ${intent.action}");
log("data ${intent.data}");
log("categories ${intent.categories}");
log("extra ${intent.extra}");
}
return intent;
}
Future<void> dohandleInitialIntent(
BuildContext context, receiveIntent.Intent? intent) async {
if (intent != null) {
if (StringUtil.isNotBlank(intent.data) &&
intent.data!.startsWith(PREFIX)) {
// This is an android signer intent

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import 'package:nowser/component/bookmark_edit_dialog.dart';
import 'package:nowser/data/bookmark_db.dart';
import 'package:nowser/util/router_util.dart';
@@ -74,12 +75,42 @@ class WebProvider extends ChangeNotifier {
}
}
void addTab() {
webInfos.add(WebInfo(_rndId(), ""));
void addTab({String url = ""}) {
webInfos.add(WebInfo(_rndId(), url));
index = webInfos.length - 1;
notifyListeners();
}
void checkAndOpenUrl(String url) {
if (!url.startsWith("http")) {
return;
}
int targetIndex = -1;
for (var i = 0; i < webInfos.length; i++) {
var webInfo = webInfos[i];
if (webInfo.url.contains(url)) {
targetIndex = i;
break;
}
}
if (targetIndex > -1) {
if (index != targetIndex) {
index = targetIndex;
notifyListeners();
}
} else {
var _currentWebInfo = currentWebInfo();
if (_currentWebInfo != null && _currentWebInfo.url == "") {
_currentWebInfo.url = url;
notifyListeners();
} else {
addTab(url: url);
}
}
}
void changeIndex(WebInfo webInfo) {
for (var i = 0; i < webInfos.length; i++) {
var owi = webInfos[i];
@@ -146,7 +177,7 @@ class WebProvider extends ChangeNotifier {
} catch (e) {}
}
void addBookmark(WebInfo webInfo) {
void addBookmark(BuildContext context, WebInfo webInfo) {
if (webInfo.browserHistory == null) {
return;
}
@@ -159,7 +190,8 @@ class WebProvider extends ChangeNotifier {
bookmark.addedToIndex = -1;
bookmark.createdAt = DateTime.now().millisecondsSinceEpoch ~/ 1000;
BookmarkDB.insert(bookmark);
// BookmarkDB.insert(bookmark);
BookmarkEditDialog.show(context, bookmark);
}
void back(BuildContext context) {

View File

@@ -1,4 +1,11 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_pinned_shortcut_plus/flutter_pinned_shortcut_plus.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import 'package:nowser/component/bookmark_edit_dialog.dart';
import 'package:nowser/component/cust_state.dart';
import 'package:nowser/component/deletable_list_mixin.dart';
import 'package:nowser/component/url_list_item_componnet.dart';
@@ -21,6 +28,10 @@ class _BookmarkRouter extends CustState<BookmarkRouter>
@override
Future<void> onReady(BuildContext context) async {
reload();
}
Future<void> reload() async {
bookmarks = await BookmarkDB.all();
setState(() {});
}
@@ -59,6 +70,36 @@ class _BookmarkRouter extends CustState<BookmarkRouter>
url: bookmark.url ?? "",
);
List<Widget> slidableActionList = [];
if (Platform.isAndroid) {
slidableActionList.add(SlidableAction(
onPressed: (context) {
doAddPinnedShortcut(bookmark);
},
backgroundColor: Colors.green,
foregroundColor: Colors.white,
icon: Icons.add_home,
label: 'Desktop',
));
}
slidableActionList.add(SlidableAction(
onPressed: (context) {
doEdit(bookmark);
},
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
icon: Icons.edit,
label: 'Edit',
));
main = Slidable(
endActionPane: ActionPane(
motion: ScrollMotion(),
children: slidableActionList,
),
child: main,
);
main = wrapListItem(main, onTap: () {
RouterUtil.back(context, bookmark.url);
}, onSelect: () {
@@ -86,4 +127,36 @@ class _BookmarkRouter extends CustState<BookmarkRouter>
selectedIds.clear();
}
}
final _flutterPinnedShortcutPlugin = FlutterPinnedShortcut();
Future<void> doAddPinnedShortcut(Bookmark bookmark) async {
File? file;
if (StringUtil.isNotBlank(bookmark.favicon)) {
// use favicon as icon
file = await DefaultCacheManager().getSingleFile(bookmark.favicon!);
} else {
// use default image as icon
// file =
print("favicon not found!");
return;
}
if (StringUtil.isBlank(bookmark.title) ||
StringUtil.isBlank(bookmark.url)) {
return;
}
_flutterPinnedShortcutPlugin.createPinnedShortcut(
id: StringUtil.rndNameStr(10),
label: bookmark.title!,
action: bookmark.url!,
iconAssetName: "assets/logo_android.png",
iconUri: Uri.file(file.path).toString());
}
Future<void> doEdit(Bookmark bookmark) async {
await BookmarkEditDialog.show(context, bookmark);
reload();
}
}

View File

@@ -40,15 +40,53 @@ class _IndexRouter extends CustState<IndexRouter>
// start build-in
buildInRelayProvider.start();
if (PlatformUtil.isAndroid()) {
var intent = await getInitialIntent();
if (intent != null) {
if (intent.categories != null &&
intent.categories!.contains("android.intent.category.LAUNCHER") &&
intent.extra != null &&
intent.extra!["flutter_pinned_shortcuts"] != null) {
var url = intent.extra!["flutter_pinned_shortcuts"];
print("find url! $url");
webProvider.checkAndOpenUrl(url);
} else {
dohandleInitialIntent(context, intent);
}
}
}
quickActions.initialize((shortcutType) {
print("find quickAction $shortcutType");
webProvider.checkAndOpenUrl(shortcutType);
});
}
@override
Widget doBuild(BuildContext context) {
if (PlatformUtil.isAndroid()) {
WidgetsBinding.instance.addPostFrameCallback((_) {
handleInitialIntent(context);
});
}
// if (PlatformUtil.isAndroid()) {
// WidgetsBinding.instance.addPostFrameCallback((_) async {
// var intent = await getInitialIntent();
// if (intent != null) {
// if (intent.categories != null &&
// intent.categories!.contains("android.intent.category.LAUNCHER") &&
// intent.extra != null &&
// intent.extra!["flutter_pinned_shortcuts"] != null) {
// var url = intent.extra!["flutter_pinned_shortcuts"];
// print("find url! $url");
// webProvider.checkAndOpenUrl(url);
// } else {
// dohandleInitialIntent(context, intent);
// }
// }
// quickActions.initialize((shortcutType) {
// print("find quickAction $shortcutType");
// webProvider.checkAndOpenUrl(shortcutType);
// });
// });
// }
remoteSigningProvider.updateContext(context);
webProvider.checkBlank();

View File

@@ -117,7 +117,7 @@ class _WebControlComponent extends State<WebControlComponent> {
onTap: () {
var webInfo = webProvider.currentWebInfo();
if (webInfo != null) {
webProvider.addBookmark(webInfo);
webProvider.addBookmark(context, webInfo);
widget.closeControl();
}
},

View File

@@ -0,0 +1,5 @@
import 'package:flutter/widgets.dart';
class DesktopShutcutUtil {
void create() {}
}

101
lib/util/dio_util.dart Normal file
View File

@@ -0,0 +1,101 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:cookie_jar/cookie_jar.dart';
import 'dart:convert' as convert;
Dio? _dio;
var cookieJar = CookieJar();
class DioUtil {
static Dio getDio() {
if (_dio == null) {
_dio = Dio();
if (_dio!.httpClientAdapter is IOHttpClientAdapter) {
(_dio!.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate =
(client) {
client.badCertificateCallback = (cert, host, port) {
return true;
};
};
}
// _dio!.options.connectTimeout = Duration(minutes: 1);
// _dio!.options.receiveTimeout = Duration(minutes: 1);
_dio!.options.headers["user-agent"] =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36";
_dio!.options.headers["accept-encoding"] = "gzip";
CookieManager cookieManager = CookieManager(cookieJar);
_dio!.interceptors.add(cookieManager);
}
return _dio!;
}
static setCookie(String link, String key, String value) {
cookieJar.saveFromResponse(Uri.parse(link), [Cookie(key, value)]);
}
static Future<Map<String, dynamic>?> get(String link,
[Map<String, dynamic>? queryParameters,
Map<String, String>? header]) async {
var dio = getDio();
if (header != null) {
dio.options.headers.addAll(header);
}
Response resp = await dio.get(link, queryParameters: queryParameters);
if (resp.statusCode == 200) {
if (resp.data is String) {
return json.decode(resp.data);
}
return resp.data;
} else {
return null;
}
}
static Future<String?> getStr(String link,
[Map<String, dynamic>? queryParameters,
Map<String, String>? header]) async {
var dio = getDio();
if (header != null) {
dio.options.headers.addAll(header);
}
Response resp =
await dio.get<String>(link, queryParameters: queryParameters);
if (resp.statusCode == 200) {
return resp.data;
} else {
return null;
}
}
static Future<List<int>?> getBytes(String link,
[Map<String, dynamic>? queryParameters,
Map<String, String>? header]) async {
var dio = getDio();
if (header != null) {
dio.options.headers.addAll(header);
}
Response resp =
await dio.get<List<int>>(link, queryParameters: queryParameters);
if (resp.statusCode == 200) {
return resp.data;
} else {
return null;
}
}
static Future<Map<String, dynamic>> post(
String link, Map<String, dynamic> parameters,
[Map<String, String>? header]) async {
var dio = getDio();
if (header != null) {
dio.options.headers.addAll(header);
}
Response resp = await dio.post(link, data: parameters);
return resp.data;
}
}

View File

@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
class MediaDataCache {
late Size size;
late EdgeInsets padding;
void update(BuildContext context) {
var mediaData = MediaQuery.of(context);
size = mediaData.size;
padding = mediaData.padding;
}
}

9
lib/util/theme_util.dart Normal file
View File

@@ -0,0 +1,9 @@
// Add some support to get value from theme data.
import 'package:flutter/material.dart';
class ThemeUtil {
static Color getDialogCoverColor(ThemeData themeData) {
return (themeData.textTheme.bodyMedium!.color ?? Colors.black)
.withOpacity(0.2);
}
}

View File

@@ -271,7 +271,7 @@ packages:
source: sdk
version: "0.0.0"
flutter_cache_manager:
dependency: transitive
dependency: "direct main"
description:
name: flutter_cache_manager
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
@@ -371,6 +371,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_pinned_shortcut_plus:
dependency: "direct main"
description:
name: flutter_pinned_shortcut_plus
sha256: "03d5e6bf8ca157e2b478e0b38a7bf6020b6646d8050e324fc1001f717b365e7c"
url: "https://pub.dev"
source: hosted
version: "0.0.2"
flutter_secure_storage:
dependency: "direct main"
description:
@@ -756,6 +764,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
quick_actions:
dependency: "direct main"
description:
name: quick_actions
sha256: "2c1d9a91f3218b4e987a7e1e95ba0415b7f48a2cb3ffacc027a1e3d3c117223f"
url: "https://pub.dev"
source: hosted
version: "1.0.8"
quick_actions_android:
dependency: transitive
description:
name: quick_actions_android
sha256: "926e50d6f879287b34d21934e6c9457f0d851f554179f2a9e8136c4acd1b7062"
url: "https://pub.dev"
source: hosted
version: "1.0.18"
quick_actions_ios:
dependency: transitive
description:
name: quick_actions_ios
sha256: "402596dea62a1028960b93f7651ec22be0e2a91e4fbf92a1c62d3b95f8ff95a5"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
quick_actions_platform_interface:
dependency: transitive
description:
name: quick_actions_platform_interface
sha256: "1fec7068db5122cd019e9340d3d7be5d36eab099695ef3402c7059ee058329a4"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
receive_intent:
dependency: "direct main"
description:

View File

@@ -52,6 +52,9 @@ dependencies:
qr_code_scanner: ^1.0.1
flutter_slidable: ^3.1.1
window_manager: ^0.4.2
quick_actions: ^1.0.8
flutter_pinned_shortcut_plus: ^0.0.2
flutter_cache_manager: ^3.4.1
dev_dependencies:
flutter_launcher_icons: ^0.13.1