diff --git a/flutter/lib/components/layout/app_bar.dart b/flutter/lib/components/layout/app_bar.dart index 4c3218f..93d3e95 100644 --- a/flutter/lib/components/layout/app_bar.dart +++ b/flutter/lib/components/layout/app_bar.dart @@ -11,46 +11,64 @@ class SCNAppBar extends StatelessWidget implements PreferredSizeWidget { required this.showThemeSwitch, required this.showDebug, required this.showSearch, + required this.showShare, + this.onShare = null, }) : super(key: key); final String? title; final bool showThemeSwitch; final bool showDebug; final bool showSearch; + final bool showShare; + final void Function()? onShare; @override Widget build(BuildContext context) { + var actions = []; + + if (showThemeSwitch) { + actions.add(Consumer( + builder: (context, appTheme, child) => IconButton( + icon: Icon(appTheme.darkMode ? FontAwesomeIcons.solidSun : FontAwesomeIcons.solidMoon), + tooltip: appTheme.darkMode ? 'Light mode' : 'Dark mode', + onPressed: appTheme.switchDarkMode, + ), + )); + } else { + actions.add(SizedBox.square(dimension: 40)); + } + + if (showDebug) { + actions.add(IconButton( + icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow), + tooltip: 'Debug', + onPressed: () { + Navigator.push(context, MaterialPageRoute(builder: (context) => DebugMainPage())); + }, + )); + } else { + actions.add(SizedBox.square(dimension: 40)); + } + + if (showSearch) { + actions.add(IconButton( + icon: const Icon(FontAwesomeIcons.solidMagnifyingGlass), + tooltip: 'Search', + onPressed: () {/*TODO*/}, + )); + } else if (showShare) { + actions.add(IconButton( + icon: const Icon(FontAwesomeIcons.solidShareNodes), + tooltip: 'Share', + onPressed: onShare ?? () {}, + )); + } else { + actions.add(SizedBox.square(dimension: 40)); + } + return AppBar( title: Text(title ?? 'Simple Cloud Notifier 2.0'), - actions: [ - if (showThemeSwitch) - Consumer( - builder: (context, appTheme, child) => IconButton( - icon: Icon(appTheme.darkMode ? FontAwesomeIcons.solidSun : FontAwesomeIcons.solidMoon), - tooltip: 'Debug', - onPressed: () { - appTheme.switchDarkMode(); - }, - ), - ), - if (!showThemeSwitch) SizedBox.square(dimension: 40), - if (showDebug) - IconButton( - icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow), - tooltip: 'Debug', - onPressed: () { - Navigator.push(context, MaterialPageRoute(builder: (context) => DebugMainPage())); - }, - ), - if (!showDebug) SizedBox.square(dimension: 40), - if (showSearch) - IconButton( - icon: const Icon(FontAwesomeIcons.solidMagnifyingGlass), - tooltip: 'Search', - onPressed: () {}, - ), - if (!showSearch) SizedBox.square(dimension: 40), - ], + actions: actions, backgroundColor: Theme.of(context).secondaryHeaderColor, ); } diff --git a/flutter/lib/components/layout/scaffold.dart b/flutter/lib/components/layout/scaffold.dart index aca4f23..ad8aeed 100644 --- a/flutter/lib/components/layout/scaffold.dart +++ b/flutter/lib/components/layout/scaffold.dart @@ -9,6 +9,8 @@ class SCNScaffold extends StatelessWidget { this.showThemeSwitch = true, this.showDebug = true, this.showSearch = true, + this.showShare = false, + this.onShare = null, }) : super(key: key); final Widget child; @@ -16,6 +18,8 @@ class SCNScaffold extends StatelessWidget { final bool showThemeSwitch; final bool showDebug; final bool showSearch; + final bool showShare; + final void Function()? onShare; @override Widget build(BuildContext context) { @@ -25,6 +29,8 @@ class SCNScaffold extends StatelessWidget { showThemeSwitch: showThemeSwitch, showDebug: showDebug, showSearch: showSearch, + showShare: showShare, + onShare: onShare ?? () {}, ), body: child, ); diff --git a/flutter/lib/nav_layout.dart b/flutter/lib/nav_layout.dart index f3de1ae..88e1af2 100644 --- a/flutter/lib/nav_layout.dart +++ b/flutter/lib/nav_layout.dart @@ -61,6 +61,7 @@ class _SCNNavLayoutState extends State { title: null, showDebug: true, showSearch: _selectedIndex == 0 || _selectedIndex == 1, + showShare: false, showThemeSwitch: true, ), body: IndexedStack( diff --git a/flutter/lib/pages/message_list/message_list.dart b/flutter/lib/pages/message_list/message_list.dart index cce810c..3281e7e 100644 --- a/flutter/lib/pages/message_list/message_list.dart +++ b/flutter/lib/pages/message_list/message_list.dart @@ -28,6 +28,7 @@ class _MessageListPageState extends State { @override void initState() { + //TODO init with state from cache - also allow tho show cache on error _pagingController.addPageRequestListener((pageKey) { _fetchPage(pageKey); }); diff --git a/flutter/lib/pages/message_view/message_view.dart b/flutter/lib/pages/message_view/message_view.dart index 742671b..3d5c9cb 100644 --- a/flutter/lib/pages/message_view/message_view.dart +++ b/flutter/lib/pages/message_view/message_view.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/api/api_exception.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart'; @@ -10,6 +12,7 @@ import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/keytoken.dart'; import 'package:simplecloudnotifier/models/message.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; +import 'package:simplecloudnotifier/utils/toaster.dart'; import 'package:simplecloudnotifier/utils/ui.dart'; class MessageViewPage extends StatefulWidget { @@ -23,8 +26,11 @@ class MessageViewPage extends StatefulWidget { class _MessageViewPageState extends State { late Future<(Message, ChannelWithSubscription?, KeyToken?)>? mainFuture; + (Message, ChannelWithSubscription?, KeyToken?)? mainFutureSnapshot = null; static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm'); + bool _monospaceMode = false; + @override void initState() { super.initState(); @@ -38,7 +44,7 @@ class _MessageViewPageState extends State { ChannelWithSubscription? chn = null; try { - chn = await APIClient.getChannel(acc, msg.channelID); + chn = await APIClient.getChannel(acc, msg.channelID); //TODO getShortChannel (?) -> no perm } on APIException catch (e) { if (e.error == APIError.USER_AUTH_FAILED) { chn = null; @@ -49,7 +55,7 @@ class _MessageViewPageState extends State { KeyToken? tok = null; try { - tok = await APIClient.getKeyToken(acc, msg.usedKeyID); + tok = await APIClient.getKeyToken(acc, msg.usedKeyID); //TODO getShortKeyToken (?) -> no perm } on APIException catch (e) { if (e.error == APIError.USER_AUTH_FAILED) { tok = null; @@ -58,7 +64,15 @@ class _MessageViewPageState extends State { } } - return (msg, chn, tok); + //TODO getShortUser for sender + + //await Future.delayed(const Duration(seconds: 2), () {}); + + final r = (msg, chn, tok); + + mainFutureSnapshot = r; + + return r; } @override @@ -71,19 +85,18 @@ class _MessageViewPageState extends State { return SCNScaffold( title: 'Message', showSearch: false, - //TODO showShare: true + showShare: true, + onShare: _share, child: FutureBuilder<(Message, ChannelWithSubscription?, KeyToken?)>( future: mainFuture, builder: (context, snapshot) { if (snapshot.hasData) { - final msg = snapshot.data!.$1; - final chn = snapshot.data!.$2; - final tok = snapshot.data!.$3; - return _buildMessageView(context, msg, chn, tok); + final (msg, chn, tok) = snapshot.data!; + return _buildMessageView(context, msg, chn, tok, false); } else if (snapshot.hasError) { return Center(child: Text('${snapshot.error}')); //TODO nice error page } else if (!widget.message.trimmed) { - return _buildLoadingView(context, widget.message); + return _buildMessageView(context, widget.message, null, null, true); } else { return const Center(child: CircularProgressIndicator()); } @@ -92,39 +105,58 @@ class _MessageViewPageState extends State { ); } - Widget _buildMessageView(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token) { - //TODO loading true/false indicator + void _share() async { + var msg = widget.message; + if (mainFutureSnapshot != null) { + (msg, _, _) = mainFutureSnapshot!; + } + + if (msg.content != null) { + final result = await Share.share(msg.content!, subject: msg.title); + + if (result.status == ShareResultStatus.unavailable) { + Toaster.error('Error', "Failed to open share dialog"); + } + } else { + final result = await Share.share(msg.title); + + if (result.status == ShareResultStatus.unavailable) { + Toaster.error('Error', "Failed to open share dialog"); + } + } + } + + Widget _buildMessageView(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token, bool loading) { + final userAccUserID = context.select((v) => v.userID); + return SingleChildScrollView( child: Padding( padding: const EdgeInsets.fromLTRB(24, 16, 24, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - ..._buildMessageHeader(context, message, channel, token), + ..._buildMessageHeader(context, message, channel, token, loading), SizedBox(height: 8), if (message.content != null) ..._buildMessageContent(context, message, channel, token), SizedBox(height: 8), if (message.senderName != null) _buildMetaCard(context, FontAwesomeIcons.solidSignature, 'Sender', [message.senderName!], () => {/*TODO*/}), - if (token != null) _buildMetaCard(context, FontAwesomeIcons.solidGearCode, 'KeyToken', [token.keytokenID, token.name], () => {/*TODO*/}), + _buildMetaCard(context, FontAwesomeIcons.solidGearCode, 'KeyToken', [message.usedKeyID, if (token != null) token.name], () => {/*TODO*/}), _buildMetaCard(context, FontAwesomeIcons.solidIdCardClip, 'MessageID', [message.messageID, if (message.userMessageID != null) message.userMessageID!], null), - if (channel != null) _buildMetaCard(context, FontAwesomeIcons.solidSnake, 'Channel', [message.channelID, channel.channel.displayName], () => {/*TODO*/}), + _buildMetaCard(context, FontAwesomeIcons.solidSnake, 'Channel', [message.channelID, channel?.channel.displayName ?? message.channelInternalName], () => {/*TODO*/}), _buildMetaCard(context, FontAwesomeIcons.solidTimer, 'Timestamp', [message.timestamp], null), + _buildMetaCard(context, FontAwesomeIcons.solidUser, 'User', ['TODO'], () => {/*TODO*/}), //TODO + if (message.senderUserID == userAccUserID) UI.button(text: "Delete Message", onPressed: () {/*TODO*/}, color: Colors.red[900]), ], ), ), ); } - Widget _buildLoadingView(BuildContext context, Message message) { - //TODO loading / skeleton use limitdata - return SizedBox(); - } - String _resolveChannelName(ChannelWithSubscription? channel, Message message) { return channel?.channel.displayName ?? message.channelInternalName; } - List _buildMessageHeader(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token) { + List _buildMessageHeader(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token, bool loading) { return [ Row( children: [ @@ -139,7 +171,24 @@ class _MessageViewPageState extends State { ], ), SizedBox(height: 8), - Text(message.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + if (!loading) Text(message.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + if (loading) + Stack( + children: [ + Row( + children: [ + Flexible(child: Text(message.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold))), + SizedBox(height: 20, width: 20), + ], + ), + Row( + children: [ + Expanded(child: SizedBox(width: 0)), + SizedBox(child: CircularProgressIndicator(), height: 20, width: 20), + ], + ), + ], + ), ]; } @@ -147,22 +196,45 @@ class _MessageViewPageState extends State { return [ Row( children: [ + if (message.priority == 2) FaIcon(FontAwesomeIcons.solidTriangleExclamation, size: 16, color: Colors.red[900]), + if (message.priority == 0) FaIcon(FontAwesomeIcons.solidDown, size: 16, color: Colors.lightBlue[900]), Expanded(child: SizedBox()), UI.buttonIconOnly( - onPressed: () {/*TODO*/}, + onPressed: () { + Clipboard.setData(new ClipboardData(text: message.content ?? '')); + Toaster.info("Clipboard", 'Copied text to Clipboard'); + }, icon: FontAwesomeIcons.copy, ), UI.buttonIconOnly( - icon: FontAwesomeIcons.lineColumns, - onPressed: () {/*TODO*/}, + icon: _monospaceMode ? FontAwesomeIcons.lineColumns : FontAwesomeIcons.alignLeft, + onPressed: () { + setState(() { + _monospaceMode = !_monospaceMode; + }); + }, ), ], ), - UI.box( - context: context, - padding: const EdgeInsets.all(4), - child: Text(message.content ?? ''), - ), + _monospaceMode + ? UI.box( + context: context, + padding: const EdgeInsets.all(4), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Text( + message.content ?? '', + style: TextStyle(fontFamily: "monospace", fontFamilyFallback: ["Courier"]), + ), + ), + borderColor: (message.priority == 2) ? Colors.red[900] : null, + ) + : UI.box( + context: context, + padding: const EdgeInsets.all(4), + child: Text(message.content ?? ''), + borderColor: (message.priority == 2) ? Colors.red[900] : null, + ) ]; } diff --git a/flutter/lib/state/app_auth.dart b/flutter/lib/state/app_auth.dart index 5000c91..c9d2e8b 100644 --- a/flutter/lib/state/app_auth.dart +++ b/flutter/lib/state/app_auth.dart @@ -1,5 +1,4 @@ import 'package:flutter/foundation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/api/api_exception.dart'; import 'package:simplecloudnotifier/models/client.dart'; @@ -63,6 +62,8 @@ class AppAuth extends ChangeNotifier implements TokenSource { } void load() { + //final cdat = Globals().sharedPrefs.getString('auth.cdate'); + //final mdat = Globals().sharedPrefs.getString('auth.mdate'); final uid = Globals().sharedPrefs.getString('auth.userid'); final cid = Globals().sharedPrefs.getString('auth.clientid'); final toka = Globals().sharedPrefs.getString('auth.tokenadmin'); @@ -85,17 +86,23 @@ class AppAuth extends ChangeNotifier implements TokenSource { } Future save() async { - final prefs = await SharedPreferences.getInstance(); if (_clientID == null || _userID == null || _tokenAdmin == null || _tokenSend == null) { - await prefs.remove('auth.userid'); - await prefs.remove('auth.tokenadmin'); - await prefs.remove('auth.tokensend'); + await Globals().sharedPrefs.remove('auth.userid'); + await Globals().sharedPrefs.remove('auth.clientid'); + await Globals().sharedPrefs.remove('auth.tokenadmin'); + await Globals().sharedPrefs.remove('auth.tokensend'); + await Globals().sharedPrefs.setString('auth.cdate', ""); + await Globals().sharedPrefs.setString('auth.mdate', DateTime.now().toIso8601String()); } else { - await prefs.setString('auth.userid', _userID!); - await prefs.setString('auth.clientid', _clientID!); - await prefs.setString('auth.tokenadmin', _tokenAdmin!); - await prefs.setString('auth.tokensend', _tokenSend!); + await Globals().sharedPrefs.setString('auth.userid', _userID!); + await Globals().sharedPrefs.setString('auth.clientid', _clientID!); + await Globals().sharedPrefs.setString('auth.tokenadmin', _tokenAdmin!); + await Globals().sharedPrefs.setString('auth.tokensend', _tokenSend!); + if (Globals().sharedPrefs.getString('auth.cdate') == null) await Globals().sharedPrefs.setString('auth.cdate', DateTime.now().toIso8601String()); + await Globals().sharedPrefs.setString('auth.mdate', DateTime.now().toIso8601String()); } + + Globals().sharedPrefs.setString('auth.mdate', DateTime.now().toIso8601String()); } Future loadUser({bool force = false}) async { diff --git a/flutter/lib/utils/ui.dart b/flutter/lib/utils/ui.dart index c95e988..e7d57cd 100644 --- a/flutter/lib/utils/ui.dart +++ b/flutter/lib/utils/ui.dart @@ -96,11 +96,11 @@ class UI { ); } - static Widget box({required BuildContext context, required Widget child, required EdgeInsets? padding}) { + static Widget box({required BuildContext context, required Widget child, required EdgeInsets? padding, Color? borderColor = null}) { return Container( padding: padding ?? EdgeInsets.all(4), decoration: BoxDecoration( - border: Border.all(color: Theme.of(context).hintColor), + border: Border.all(color: borderColor ?? Theme.of(context).hintColor), borderRadius: BorderRadius.circular(DefaultBorderRadius), ), child: child, diff --git a/flutter/macos/Flutter/GeneratedPluginRegistrant.swift b/flutter/macos/Flutter/GeneratedPluginRegistrant.swift index 05dd87d..d39b825 100644 --- a/flutter/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/flutter/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,6 +10,7 @@ import firebase_core import firebase_messaging import package_info_plus import path_provider_foundation +import share_plus import shared_preferences_foundation import url_launcher_macos @@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index a98ce15..4c21d1b 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -161,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + url: "https://pub.dev" + source: hosted + version: "0.3.4+1" crypto: dependency: transitive description: @@ -687,6 +695,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544 + url: "https://pub.dev" + source: hosted + version: "9.0.0" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4" + url: "https://pub.dev" + source: hosted + version: "4.0.0" shared_preferences: dependency: "direct main" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a56972a..a47d181 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: device_info_plus: ^10.1.0 toastification: ^2.0.0 uuid: ^4.4.0 + share_plus: ^9.0.0 dependency_overrides: diff --git a/flutter/windows/flutter/generated_plugin_registrant.cc b/flutter/windows/flutter/generated_plugin_registrant.cc index ec8e8d4..c3d3b6a 100644 --- a/flutter/windows/flutter/generated_plugin_registrant.cc +++ b/flutter/windows/flutter/generated_plugin_registrant.cc @@ -7,11 +7,14 @@ #include "generated_plugin_registrant.h" #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/flutter/windows/flutter/generated_plugins.cmake b/flutter/windows/flutter/generated_plugins.cmake index 02d26c3..c04ddae 100644 --- a/flutter/windows/flutter/generated_plugins.cmake +++ b/flutter/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST firebase_core + share_plus url_launcher_windows )