Upgrade Flutter

This commit is contained in:
Hillel Coren 2023-02-26 17:27:06 +02:00
parent a9393134b8
commit ca278c046a
26 changed files with 1180 additions and 654 deletions

View File

@ -4,7 +4,7 @@
# This file should be version controlled. # This file should be version controlled.
version: version:
revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c revision: c07f7888888435fd9df505aa2efc38d3cf65681b
channel: stable channel: stable
project_type: app project_type: app
@ -13,11 +13,11 @@ project_type: app
migration: migration:
platforms: platforms:
- platform: root - platform: root
create_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c create_revision: c07f7888888435fd9df505aa2efc38d3cf65681b
base_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c base_revision: c07f7888888435fd9df505aa2efc38d3cf65681b
- platform: macos - platform: linux
create_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c create_revision: c07f7888888435fd9df505aa2efc38d3cf65681b
base_revision: 4f9d92fbbdf072a70a70d2179a9f87392b94104c base_revision: c07f7888888435fd9df505aa2efc38d3cf65681b
# User provided section # User provided section

View File

@ -152,10 +152,13 @@ class InvoiceNinjaAppState extends State<InvoiceNinjaApp> {
try { try {
authenticated = await LocalAuthentication().authenticate( authenticated = await LocalAuthentication().authenticate(
localizedReason: 'Please authenticate to access the app', localizedReason: 'Please authenticate to access the app',
options: const AuthenticationOptions(
biometricOnly: true, biometricOnly: true,
useErrorDialogs: true, useErrorDialogs: true,
stickyAuth: false); stickyAuth: false,
),
);
} catch (e) { } catch (e) {
print(e); print(e);
} }
@ -384,6 +387,11 @@ class InvoiceNinjaAppState extends State<InvoiceNinjaApp> {
? LockScreen(onAuthenticatePressed: _authenticate) ? LockScreen(onAuthenticatePressed: _authenticate)
: InitScreen(), : InitScreen(),
locale: locale, locale: locale,
/*
theme: state.prefState.enableDarkMode
? ThemeData.dark(useMaterial3: true)
: ThemeData.light(useMaterial3: true),
*/
theme: state.prefState.enableDarkMode theme: state.prefState.enableDarkMode
? ThemeData( ? ThemeData(
colorScheme: ColorScheme.dark().copyWith( colorScheme: ColorScheme.dark().copyWith(

View File

@ -14,7 +14,7 @@ import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:share/share.dart'; import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
// Project imports: // Project imports:
@ -227,7 +227,7 @@ class DocumentTile extends StatelessWidget {
await File(filePath) await File(filePath)
.writeAsBytes(response.bodyBytes); .writeAsBytes(response.bodyBytes);
await Share.shareFiles([filePath]); await Share.shareXFiles([XFile(filePath)]);
} }
} else if (value == localization.delete) { } else if (value == localization.delete) {
confirmCallback( confirmCallback(

View File

@ -1400,7 +1400,7 @@ void _showAbout(BuildContext context) async {
padding: const EdgeInsets.only(top: 4), padding: const EdgeInsets.only(top: 4),
child: AppButton( child: AppButton(
label: localization.appPlatforms.toUpperCase(), label: localization.appPlatforms.toUpperCase(),
iconData: MdiIcons.desktopMac, iconData: MdiIcons.desktopClassic,
onPressed: () { onPressed: () {
showDialog<AlertDialog>( showDialog<AlertDialog>(
context: context, context: context,

View File

@ -22,7 +22,6 @@ import 'package:invoiceninja_flutter/ui/app/forms/date_picker.dart';
import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:printing/printing.dart'; import 'package:printing/printing.dart';
import 'package:share/share.dart';
// Project imports: // Project imports:
import 'package:invoiceninja_flutter/data/models/dashboard_model.dart'; import 'package:invoiceninja_flutter/data/models/dashboard_model.dart';
@ -40,6 +39,7 @@ import 'package:invoiceninja_flutter/utils/platforms.dart';
import 'package:invoiceninja_flutter/utils/web_stub.dart' import 'package:invoiceninja_flutter/utils/web_stub.dart'
if (dart.library.html) 'package:invoiceninja_flutter/utils/web.dart'; if (dart.library.html) 'package:invoiceninja_flutter/utils/web.dart';
import 'package:share_plus/share_plus.dart';
class ClientPdfView extends StatefulWidget { class ClientPdfView extends StatefulWidget {
const ClientPdfView({ const ClientPdfView({
@ -404,7 +404,7 @@ class _ClientPdfViewState extends State<ClientPdfView> {
showToast( showToast(
localization.fileSavedInDownloadsFolder); localization.fileSavedInDownloadsFolder);
} else { } else {
await Share.shareFiles([filePath]); await Share.shareXFiles([XFile(filePath)]);
} }
} }
}, },
@ -439,37 +439,39 @@ class _ClientPdfViewState extends State<ClientPdfView> {
callback: (_) => loadPDF(sendEmail: true)); callback: (_) => loadPDF(sendEmail: true));
}, },
), ),
TextButton( if (supportsSchedules())
onPressed: () { TextButton(
if (!state.isProPlan) { onPressed: () {
showMessageDialog( if (!state.isProPlan) {
context: context, showMessageDialog(
message: localization.upgradeToPaidPlanToSchedule, context: context,
secondaryActions: [ message: localization.upgradeToPaidPlanToSchedule,
TextButton( secondaryActions: [
onPressed: () { TextButton(
store.dispatch(ViewSettings( onPressed: () {
section: kSettingsAccountManagement)); store.dispatch(ViewSettings(
Navigator.of(context).pop(); section: kSettingsAccountManagement));
}, Navigator.of(context).pop();
child: },
Text(localization.upgrade.toUpperCase())), child: Text(
]); localization.upgrade.toUpperCase())),
return; ]);
} return;
}
createEntity( createEntity(
context: context, context: context,
entity: ScheduleEntity().rebuild((b) => b entity: ScheduleEntity().rebuild((b) => b
..template = ScheduleEntity.TEMPLATE_EMAIL_STATEMENT ..template =
..parameters.clients.add(client.id) ScheduleEntity.TEMPLATE_EMAIL_STATEMENT
..parameters.showAgingTable = _showAging ..parameters.clients.add(client.id)
..parameters.showPaymentsTable = _showPayments ..parameters.showAgingTable = _showAging
..parameters.status = _status ..parameters.showPaymentsTable = _showPayments
..parameters.dateRange = _dateRange.snakeCase)); ..parameters.status = _status
}, ..parameters.dateRange = _dateRange.snakeCase));
child: Text(localization.schedule, },
style: TextStyle(color: state.headerTextColor))), child: Text(localization.schedule,
style: TextStyle(color: state.headerTextColor))),
if (isDesktop(context)) if (isDesktop(context))
TextButton( TextButton(
child: Text(localization.close, child: Text(localization.close,

View File

@ -16,7 +16,7 @@ import 'package:invoiceninja_flutter/main_app.dart';
import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart'; import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:printing/printing.dart'; import 'package:printing/printing.dart';
import 'package:share/share.dart'; import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
// Project imports: // Project imports:
@ -282,7 +282,7 @@ class _InvoicePdfViewState extends State<InvoicePdfView> {
showToast(localization showToast(localization
.fileSavedInDownloadsFolder); .fileSavedInDownloadsFolder);
} else { } else {
await Share.shareFiles([filePath]); await Share.shareXFiles([XFile(filePath)]);
} }
} }
} }

View File

@ -21,7 +21,6 @@ import 'package:invoiceninja_flutter/utils/platforms.dart';
import 'package:memoize/memoize.dart'; import 'package:memoize/memoize.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:redux/redux.dart'; import 'package:redux/redux.dart';
import 'package:share/share.dart';
// Project imports: // Project imports:
import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/constants.dart';
@ -51,6 +50,7 @@ import 'package:invoiceninja_flutter/utils/formatting.dart';
import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:invoiceninja_flutter/utils/money.dart'; import 'package:invoiceninja_flutter/utils/money.dart';
import 'package:invoiceninja_flutter/utils/strings.dart'; import 'package:invoiceninja_flutter/utils/strings.dart';
import 'package:share_plus/share_plus.dart';
import 'credit_report.dart'; import 'credit_report.dart';
import 'package:invoiceninja_flutter/utils/web_stub.dart' import 'package:invoiceninja_flutter/utils/web_stub.dart'
@ -515,7 +515,7 @@ class ReportsScreenVM {
if (isDesktopOS()) { if (isDesktopOS()) {
showToast(localization.fileSavedInDownloadsFolder); showToast(localization.fileSavedInDownloadsFolder);
} else { } else {
await Share.shareFiles([filePath]); await Share.shareXFiles([XFile(filePath)]);
} }
} }
}); });

View File

@ -152,9 +152,10 @@ class DeviceSettingsVM {
authenticated = await LocalAuthentication().authenticate( authenticated = await LocalAuthentication().authenticate(
localizedReason: localizedReason:
AppLocalization.of(context).authenticateToChangeSetting, AppLocalization.of(context).authenticateToChangeSetting,
biometricOnly: true, options: const AuthenticationOptions(
useErrorDialogs: true, biometricOnly: true,
stickyAuth: false); useErrorDialogs: true,
stickyAuth: false));
} catch (e) { } catch (e) {
print(e); print(e);
} }

View File

@ -240,10 +240,11 @@ class _SettingsListState extends State<SettingsList> {
), ),
*/ */
if (showAll) ...[ if (showAll) ...[
SettingsListTile( if (supportsSchedules())
section: kSettingsSchedules, SettingsListTile(
viewModel: widget.viewModel, section: kSettingsSchedules,
), viewModel: widget.viewModel,
),
SettingsListTile( SettingsListTile(
section: kSettingsUserManagement, section: kSettingsUserManagement,
viewModel: widget.viewModel, viewModel: widget.viewModel,
@ -623,11 +624,12 @@ class SettingsSearch extends StatelessWidget {
'subscriptions', 'subscriptions',
], ],
], ],
kSettingsSchedules: [ if (supportsSchedules())
[ kSettingsSchedules: [
'schedules#2023-02-15', [
'schedules#2023-02-15',
],
], ],
],
kSettingsUserManagement: [ kSettingsUserManagement: [
[ [
'users', 'users',

View File

@ -31,6 +31,12 @@ bool supportsAppleOAuth() => kIsWeb || isApple();
// TODO remove this function // TODO remove this function
bool supportsMicrosoftOAuth() => kIsWeb; bool supportsMicrosoftOAuth() => kIsWeb;
// TODO remove this
bool supportsSchedules() {
final store = StoreProvider.of<AppState>(navigatorKey.currentContext);
return store.state.isSelfHosted;
}
bool supportsInAppPurchase() { bool supportsInAppPurchase() {
final store = StoreProvider.of<AppState>(navigatorKey.currentContext); final store = StoreProvider.of<AppState>(navigatorKey.currentContext);
if (store.state.isSelfHosted) { if (store.state.isSelfHosted) {

View File

@ -34,12 +34,20 @@ class _ExampleEditorState extends State<ExampleEditor> {
ScrollController _scrollController; ScrollController _scrollController;
final _darkBackground = const Color(0xFF222222);
final _lightBackground = Colors.white;
Brightness _brightness = Brightness.light;
SuperEditorDebugVisualsConfig _debugConfig;
OverlayEntry _textFormatBarOverlayEntry; OverlayEntry _textFormatBarOverlayEntry;
final _textSelectionAnchor = ValueNotifier<Offset>(null); final _textSelectionAnchor = ValueNotifier<Offset>(null);
OverlayEntry _imageFormatBarOverlayEntry; OverlayEntry _imageFormatBarOverlayEntry;
final _imageSelectionAnchor = ValueNotifier<Offset>(null); final _imageSelectionAnchor = ValueNotifier<Offset>(null);
final _overlayController = MagnifierAndToolbarController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -55,8 +63,10 @@ class _ExampleEditorState extends State<ExampleEditor> {
_doc = deserializeMarkdownToDocument(markdown) _doc = deserializeMarkdownToDocument(markdown)
..addListener(_hideOrShowToolbar) ..addListener(_hideOrShowToolbar)
..addListener(_onChanged); ..addListener(_onChanged);
_docEditor = DocumentEditor(document: _doc as MutableDocument); _docEditor = DocumentEditor(document: _doc as MutableDocument);
_composer = DocumentComposer()..addListener(_hideOrShowToolbar); _composer = DocumentComposer();
_composer.selectionNotifier.addListener(_hideOrShowToolbar);
_docOps = CommonEditorOperations( _docOps = CommonEditorOperations(
editor: _docEditor, editor: _docEditor,
composer: _composer, composer: _composer,
@ -169,15 +179,16 @@ class _ExampleEditorState extends State<ExampleEditor> {
void _showEditorToolbar() { void _showEditorToolbar() {
if (_textFormatBarOverlayEntry == null) { if (_textFormatBarOverlayEntry == null) {
// Create an overlay entry to build the editor toolbar.
// TODO: add an overlay to the Editor widget to avoid using the
// application overlay
_textFormatBarOverlayEntry ??= OverlayEntry(builder: (context) { _textFormatBarOverlayEntry ??= OverlayEntry(builder: (context) {
return Theme( return EditorToolbar(
data: ThemeData.light(), anchor: _textSelectionAnchor,
child: EditorToolbar( editorFocusNode: _editorFocusNode,
anchor: _textSelectionAnchor, editor: _docEditor,
editor: _docEditor, composer: _composer,
composer: _composer, closeToolbar: _hideEditorToolbar,
closeToolbar: _hideEditorToolbar,
),
); );
}); });
@ -199,7 +210,6 @@ class _ExampleEditorState extends State<ExampleEditor> {
_composer.selection.base, _composer.selection.extent); _composer.selection.base, _composer.selection.extent);
final docBox = final docBox =
_docLayoutKey.currentContext.findRenderObject() as RenderBox; _docLayoutKey.currentContext.findRenderObject() as RenderBox;
final overlayBoundingBox = Rect.fromPoints( final overlayBoundingBox = Rect.fromPoints(
docBox.localToGlobal(docBoundingBox.topLeft), docBox.localToGlobal(docBoundingBox.topLeft),
docBox.localToGlobal(docBoundingBox.bottomRight), docBox.localToGlobal(docBoundingBox.bottomRight),
@ -222,14 +232,14 @@ class _ExampleEditorState extends State<ExampleEditor> {
// and non-null implies the entry is in the overlay. // and non-null implies the entry is in the overlay.
_textFormatBarOverlayEntry.remove(); _textFormatBarOverlayEntry.remove();
_textFormatBarOverlayEntry = null; _textFormatBarOverlayEntry = null;
}
// Ensure that focus returns to the editor. // Ensure that focus returns to the editor.
// //
// I tried explicitly unfocus()'ing the URL textfield // I tried explicitly unfocus()'ing the URL textfield
// in the toolbar but it didn't return focus to the // in the toolbar but it didn't return focus to the
// editor. I'm not sure why. // editor. I'm not sure why.
_editorFocusNode.requestFocus(); _editorFocusNode.requestFocus();
}
} }
DocumentGestureMode get _gestureMode { DocumentGestureMode get _gestureMode {
@ -245,29 +255,41 @@ class _ExampleEditorState extends State<ExampleEditor> {
return DocumentGestureMode.mouse; return DocumentGestureMode.mouse;
} }
return null; return DocumentGestureMode.mouse;
} }
bool get _isMobile => _gestureMode != DocumentGestureMode.mouse; bool get _isMobile => _gestureMode != DocumentGestureMode.mouse;
DocumentInputSource get _inputSource { TextInputSource get _inputSource {
switch (defaultTargetPlatform) { switch (defaultTargetPlatform) {
case TargetPlatform.android: case TargetPlatform.android:
case TargetPlatform.iOS: case TargetPlatform.iOS:
return DocumentInputSource.ime;
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
case TargetPlatform.linux: case TargetPlatform.linux:
case TargetPlatform.macOS: case TargetPlatform.macOS:
case TargetPlatform.windows: case TargetPlatform.windows:
return DocumentInputSource.keyboard; return TextInputSource.ime;
// return DocumentInputSource.keyboard;
} }
return null; return TextInputSource.ime;
}
void _cut() {
_docOps.cut();
_overlayController.hideToolbar();
}
void _copy() {
_docOps.copy();
_overlayController.hideToolbar();
}
void _paste() {
_docOps.paste();
_overlayController.hideToolbar();
} }
void _cut() => _docOps.cut();
void _copy() => _docOps.copy();
void _paste() => _docOps.paste();
void _selectAll() => _docOps.selectAll(); void _selectAll() => _docOps.selectAll();
void _showImageToolbar() { void _showImageToolbar() {
@ -277,7 +299,7 @@ class _ExampleEditorState extends State<ExampleEditor> {
return ImageFormatToolbar( return ImageFormatToolbar(
anchor: _imageSelectionAnchor, anchor: _imageSelectionAnchor,
composer: _composer, composer: _composer,
setWidth: (dynamic nodeId, dynamic width) { setWidth: (nodeId, width) {
final node = _doc.getNodeById(nodeId); final node = _doc.getNodeById(nodeId);
final currentStyles = final currentStyles =
SingleColumnLayoutComponentStyles.fromMetadata(node); SingleColumnLayoutComponentStyles.fromMetadata(node);
@ -309,10 +331,8 @@ class _ExampleEditorState extends State<ExampleEditor> {
final docBox = final docBox =
_docLayoutKey.currentContext.findRenderObject() as RenderBox; _docLayoutKey.currentContext.findRenderObject() as RenderBox;
final overlayBoundingBox = Rect.fromPoints( final overlayBoundingBox = Rect.fromPoints(
docBox.localToGlobal(docBoundingBox.topLeft, docBox.localToGlobal(docBoundingBox.topLeft),
ancestor: context.findRenderObject()), docBox.localToGlobal(docBoundingBox.bottomRight),
docBox.localToGlobal(docBoundingBox.bottomRight,
ancestor: context.findRenderObject()),
); );
_imageSelectionAnchor.value = overlayBoundingBox.center; _imageSelectionAnchor.value = overlayBoundingBox.center;
@ -332,46 +352,152 @@ class _ExampleEditorState extends State<ExampleEditor> {
// and non-null implies the entry is in the overlay. // and non-null implies the entry is in the overlay.
_imageFormatBarOverlayEntry.remove(); _imageFormatBarOverlayEntry.remove();
_imageFormatBarOverlayEntry = null; _imageFormatBarOverlayEntry = null;
}
// Ensure that focus returns to the editor. // Ensure that focus returns to the editor.
_editorFocusNode.requestFocus(); _editorFocusNode.requestFocus();
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Theme(
children: [ data: ThemeData(brightness: _brightness),
Expanded( child: Builder(builder: (themedContext) {
child: _buildEditor(), // This builder captures the new theme
), return Stack(
if (_isMobile) _buildMountedToolbar(), children: [
], Column(
children: [
Expanded(
child: _buildEditor(themedContext),
),
if (_isMobile) _buildMountedToolbar(),
],
),
Align(
alignment: Alignment.bottomRight,
child: _buildCornerFabs(),
),
],
);
}),
); );
} }
Widget _buildEditor() { Widget _buildCornerFabs() {
return SuperEditor( return Padding(
editor: _docEditor, padding: const EdgeInsets.only(right: 16, bottom: 16),
composer: _composer, child: Column(
focusNode: _editorFocusNode, mainAxisSize: MainAxisSize.min,
scrollController: _scrollController, crossAxisAlignment: CrossAxisAlignment.center,
documentLayoutKey: _docLayoutKey, children: [
componentBuilders: [ _buildDebugVisualsToggle(),
...defaultComponentBuilders, const SizedBox(height: 16),
], _buildLightAndDarkModeToggle(),
gestureMode: _gestureMode, ],
inputSource: _inputSource,
androidToolbarBuilder: (_) => AndroidTextEditingFloatingToolbar(
onCutPressed: _cut,
onCopyPressed: _copy,
onPastePressed: _paste,
onSelectAllPressed: _selectAll,
), ),
iOSToolbarBuilder: (_) => IOSTextEditingFloatingToolbar( );
onCutPressed: _cut, }
onCopyPressed: _copy,
onPastePressed: _paste, Widget _buildDebugVisualsToggle() {
return FloatingActionButton(
backgroundColor:
_brightness == Brightness.light ? _darkBackground : _lightBackground,
foregroundColor:
_brightness == Brightness.light ? _lightBackground : _darkBackground,
elevation: 5,
onPressed: () {
setState(() {
_debugConfig = _debugConfig != null
? null
: SuperEditorDebugVisualsConfig(
showFocus: true,
showImeConnection: true,
);
});
},
child: const Icon(
Icons.bug_report,
),
);
}
Widget _buildLightAndDarkModeToggle() {
return FloatingActionButton(
backgroundColor:
_brightness == Brightness.light ? _darkBackground : _lightBackground,
foregroundColor:
_brightness == Brightness.light ? _lightBackground : _darkBackground,
elevation: 5,
onPressed: () {
setState(() {
_brightness = _brightness == Brightness.light
? Brightness.dark
: Brightness.light;
});
},
child: _brightness == Brightness.light
? const Icon(
Icons.dark_mode,
)
: const Icon(
Icons.light_mode,
),
);
}
Widget _buildEditor(BuildContext context) {
final isLight = Theme.of(context).brightness == Brightness.light;
return ColoredBox(
color: isLight ? _lightBackground : _darkBackground,
child: SuperEditorDebugVisuals(
config: _debugConfig ?? const SuperEditorDebugVisualsConfig(),
child: SuperEditor(
editor: _docEditor,
composer: _composer,
focusNode: _editorFocusNode,
scrollController: _scrollController,
documentLayoutKey: _docLayoutKey,
documentOverlayBuilders: [
DefaultCaretOverlayBuilder(
CaretStyle()
.copyWith(color: isLight ? Colors.black : Colors.redAccent),
),
],
selectionStyle: isLight
? defaultSelectionStyle
: SelectionStyles(
selectionColor: Colors.red.withOpacity(0.3),
),
stylesheet: defaultStylesheet.copyWith(
addRulesAfter: [
if (!isLight) ..._darkModeStyles,
taskStyles,
],
),
componentBuilders: [
TaskComponentBuilder(_docEditor),
...defaultComponentBuilders,
],
gestureMode: _gestureMode,
inputSource: _inputSource,
keyboardActions: _inputSource == TextInputSource.ime
? defaultImeKeyboardActions
: defaultKeyboardActions,
androidToolbarBuilder: (_) => AndroidTextEditingFloatingToolbar(
onCutPressed: _cut,
onCopyPressed: _copy,
onPastePressed: _paste,
onSelectAllPressed: _selectAll,
),
iOSToolbarBuilder: (_) => IOSTextEditingFloatingToolbar(
onCutPressed: _cut,
onCopyPressed: _copy,
onPastePressed: _paste,
),
overlayController: _overlayController,
),
), ),
); );
} }
@ -398,3 +524,37 @@ class _ExampleEditorState extends State<ExampleEditor> {
); );
} }
} }
// Makes text light, for use during dark mode styling.
final _darkModeStyles = [
StyleRule(
BlockSelector.all,
(doc, docNode) {
return <String, dynamic>{
'textStyle': const TextStyle(
color: Color(0xFFCCCCCC),
),
};
},
),
StyleRule(
const BlockSelector("header1"),
(doc, docNode) {
return <String, dynamic>{
'textStyle': const TextStyle(
color: Color(0xFF888888),
),
};
},
),
StyleRule(
const BlockSelector("header2"),
(doc, docNode) {
return <String, dynamic>{
'textStyle': const TextStyle(
color: Color(0xFF888888),
),
};
},
),
];

View File

@ -14,6 +14,7 @@ class EditorToolbar extends StatefulWidget {
const EditorToolbar({ const EditorToolbar({
Key key, Key key,
@required this.anchor, @required this.anchor,
@required this.editorFocusNode,
@required this.editor, @required this.editor,
@required this.composer, @required this.composer,
@required this.closeToolbar, @required this.closeToolbar,
@ -26,6 +27,9 @@ class EditorToolbar extends StatefulWidget {
/// reposition itself as the [Offset] value changes. /// reposition itself as the [Offset] value changes.
final ValueNotifier<Offset> anchor; final ValueNotifier<Offset> anchor;
/// The [FocusNode] attached to the editor to which this toolbar applies.
final FocusNode editorFocusNode;
/// The [editor] is used to alter document content, such as /// The [editor] is used to alter document content, such as
/// when the user selects a different block format for a /// when the user selects a different block format for a
/// text blob, e.g., paragraph, header, blockquote, or /// text blob, e.g., paragraph, header, blockquote, or
@ -49,13 +53,13 @@ class EditorToolbar extends StatefulWidget {
class _EditorToolbarState extends State<EditorToolbar> { class _EditorToolbarState extends State<EditorToolbar> {
bool _showUrlField = false; bool _showUrlField = false;
FocusNode _urlFocusNode; FocusNode _urlFocusNode;
TextEditingController _urlController; AttributedTextEditingController _urlController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_urlFocusNode = FocusNode(); _urlFocusNode = FocusNode();
_urlController = TextEditingController(); _urlController = SingleLineAttributedTextEditingController(_applyLink);
} }
@override @override
@ -112,7 +116,7 @@ class _EditorToolbarState extends State<EditorToolbar> {
/* /*
/// Returns the text alignment of the currently selected text node. /// Returns the text alignment of the currently selected text node.
/// ///
/// Throws an exception if the currently selected node is not a text node. /// Throws an exception if the currently selected node is not a text node.
TextAlign _getCurrentTextAlignment() { TextAlign _getCurrentTextAlignment() {
final selectedNode = widget.editor.document final selectedNode = widget.editor.document
.getNodeById(widget.composer.selection.extent.nodeId); .getNodeById(widget.composer.selection.extent.nodeId);
@ -352,7 +356,7 @@ class _EditorToolbarState extends State<EditorToolbar> {
/// Takes the text from the [urlController] and applies it as a link /// Takes the text from the [urlController] and applies it as a link
/// attribution to the currently selected text. /// attribution to the currently selected text.
void _applyLink() { void _applyLink() {
final url = _urlController.text; final url = _urlController.text.text;
final selection = widget.composer.selection; final selection = widget.composer.selection;
final baseOffset = (selection.base.nodePosition as TextPosition).offset; final baseOffset = (selection.base.nodePosition as TextPosition).offset;
@ -360,7 +364,7 @@ class _EditorToolbarState extends State<EditorToolbar> {
final selectionStart = min(baseOffset, extentOffset); final selectionStart = min(baseOffset, extentOffset);
final selectionEnd = max(baseOffset, extentOffset); final selectionEnd = max(baseOffset, extentOffset);
final selectionRange = final selectionRange =
SpanRange(start: selectionStart, end: selectionEnd - 1); TextRange(start: selectionStart, end: selectionEnd - 1);
final textNode = final textNode =
widget.editor.document.getNodeById(selection.extent.nodeId) as TextNode; widget.editor.document.getNodeById(selection.extent.nodeId) as TextNode;
@ -387,7 +391,7 @@ class _EditorToolbarState extends State<EditorToolbar> {
/// Given [text] and a [range] within the [text], the [range] is /// Given [text] and a [range] within the [text], the [range] is
/// shortened on both sides to remove any trailing whitespace and /// shortened on both sides to remove any trailing whitespace and
/// the new range is returned. /// the new range is returned.
SpanRange _trimTextRangeWhitespace(AttributedText text, SpanRange range) { SpanRange _trimTextRangeWhitespace(AttributedText text, TextRange range) {
int startOffset = range.start; int startOffset = range.start;
int endOffset = range.end; int endOffset = range.end;
@ -504,6 +508,9 @@ class _EditorToolbarState extends State<EditorToolbar> {
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
/*
// https://github.com/superlistapp/super_editor/issues/689
// https://github.com/flutter/flutter/issues/106923
// Only allow the user to select a new type of text node if // Only allow the user to select a new type of text node if
// the currently selected node can be converted. // the currently selected node can be converted.
if (_isConvertibleNode()) ...[ if (_isConvertibleNode()) ...[
@ -512,7 +519,6 @@ class _EditorToolbarState extends State<EditorToolbar> {
child: DropdownButton<_TextType>( child: DropdownButton<_TextType>(
value: _getCurrentTextType(), value: _getCurrentTextType(),
items: _TextType.values items: _TextType.values
.where((element) => element != _TextType.blockquote)
.map((textType) => DropdownMenuItem<_TextType>( .map((textType) => DropdownMenuItem<_TextType>(
value: textType, value: textType,
child: Padding( child: Padding(
@ -522,8 +528,10 @@ class _EditorToolbarState extends State<EditorToolbar> {
)) ))
.toList(), .toList(),
icon: const Icon(Icons.arrow_drop_down), icon: const Icon(Icons.arrow_drop_down),
style: const TextStyle( style: TextStyle(
color: Colors.black, color: Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black,
fontSize: 12, fontSize: 12,
), ),
underline: const SizedBox(), underline: const SizedBox(),
@ -534,6 +542,7 @@ class _EditorToolbarState extends State<EditorToolbar> {
), ),
_buildVerticalDivider(), _buildVerticalDivider(),
], ],
*/
Center( Center(
child: IconButton( child: IconButton(
onPressed: _toggleBold, onPressed: _toggleBold,
@ -604,6 +613,15 @@ class _EditorToolbarState extends State<EditorToolbar> {
), ),
), ),
], ],
_buildVerticalDivider(),
Center(
child: IconButton(
onPressed: () {},
icon: const Icon(Icons.more_vert),
splashRadius: 16,
tooltip: 'More Options',
),
),
*/ */
], ],
), ),
@ -623,14 +641,37 @@ class _EditorToolbarState extends State<EditorToolbar> {
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: TextField( child: FocusWithCustomParent(
focusNode: _urlFocusNode, focusNode: _urlFocusNode,
controller: _urlController, parentFocusNode: widget.editorFocusNode,
decoration: const InputDecoration( // We use a SuperTextField instead of a TextField because TextField
hintText: 'enter url...', // automatically re-parents its FocusNode, which causes #609. Flutter
border: InputBorder.none, // #106923 tracks the TextField issue.
child: SuperTextField(
focusNode: _urlFocusNode,
textController: _urlController,
minLines: 1,
maxLines: 1,
inputSource: TextInputSource.ime,
hintBehavior: HintBehavior.displayHintUntilTextEntered,
hintBuilder: (context) {
return Text(
'Enter a url...',
style: const TextStyle(
color: Colors.grey,
fontSize: 16,
),
);
},
textStyleBuilder: (_) {
return TextStyle(
color: Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black,
fontSize: 16,
);
},
), ),
onSubmitted: (newValue) => _applyLink(),
), ),
), ),
IconButton( IconButton(
@ -840,3 +881,21 @@ class _PositionedToolbar extends StatelessWidget {
); );
} }
} }
class SingleLineAttributedTextEditingController
extends AttributedTextEditingController {
SingleLineAttributedTextEditingController(this.onSubmit);
final VoidCallback onSubmit;
@override
void insertNewline() {
// Don't insert newline in a single-line text field.
// Invoke callback to take action on enter.
onSubmit();
// TODO: this is a hack. SuperTextField shouldn't insert newlines in a single
// line field (#697).
}
}

View File

@ -8,12 +8,13 @@ import Foundation
import in_app_purchase_storekit import in_app_purchase_storekit
import in_app_review import in_app_review
import package_info import package_info
import package_info_plus_macos import package_info_plus
import path_provider_macos import path_provider_foundation
import printing import printing
import screen_retriever import screen_retriever
import sentry_flutter import sentry_flutter
import shared_preferences_macos import share_plus
import shared_preferences_foundation
import sign_in_with_apple import sign_in_with_apple
import smart_auth import smart_auth
import sqflite import sqflite
@ -29,6 +30,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin")) PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin")) SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin"))
SmartAuthPlugin.register(with: registry.registrar(forPlugin: "SmartAuthPlugin")) SmartAuthPlugin.register(with: registry.registrar(forPlugin: "SmartAuthPlugin"))

View File

@ -20,16 +20,16 @@ dependencies:
sdk: flutter sdk: flutter
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
# google_sign_in: ^5.0.7 # google_sign_in: ^6.0.1
# in_app_review: ^2.0.4 # in_app_review: ^2.0.4
# in_app_purchase: ^3.1.1 # in_app_purchase: ^3.1.1
# pinput: ^2.2.11 # pinput: ^2.2.11
flutter_redux: ^0.8.2 flutter_redux: ^0.10.0
redux_logging: ^0.5.0 redux_logging: ^0.5.0
http: ^0.13.3 http: ^0.13.3
path_provider: ^2.0.2 path_provider: ^2.0.2
shared_preferences: ^2.0.6 shared_preferences: ^2.0.6
material_design_icons_flutter: ^5.0.6295 material_design_icons_flutter: ^6.0.7096
built_value: ^8.1.2 built_value: ^8.1.2
built_collection: ^5.1.0 built_collection: ^5.1.0
memoize: ^3.0.0 memoize: ^3.0.0
@ -37,46 +37,46 @@ dependencies:
url_launcher: ^6.0.20 url_launcher: ^6.0.20
share: ^2.0.4 share: ^2.0.4
intl: ^0.17.0 intl: ^0.17.0
flutter_slidable: ^1.1.0 flutter_slidable: ^2.0.0
charts_flutter: ^0.12.0 charts_flutter: ^0.12.0
#qr_flutter: ^4.0.0 #qr_flutter: ^4.0.0
qr_flutter: # https://github.com/theyakka/qr.flutter/issues/174#issuecomment-1084235757 qr_flutter: # https://github.com/theyakka/qr.flutter/issues/174#issuecomment-1084235757
git: git:
url: https://github.com/theyakka/qr.flutter.git url: https://github.com/theyakka/qr.flutter.git
local_auth: ^1.1.6 local_auth: ^2.1.5
sentry_flutter: ^6.5.1 sentry_flutter: ^6.20.1
image_picker: ^0.8.3+1 image_picker: ^0.8.6+3
flutter_colorpicker: ^1.0.3 flutter_colorpicker: ^1.0.3
flutter_json_viewer: ^1.0.1 flutter_json_viewer: ^1.0.1
webview_flutter: ^3.0.4 webview_flutter: ^3.0.4
timeago: ^3.1.0 timeago: ^3.1.0
package_info: ^2.0.2 package_info: ^2.0.2
rounded_loading_button: ^2.0.5 rounded_loading_button: ^2.1.0
version: ^2.0.0 version: ^3.0.2
flutter_launcher_icons: ^0.9.1 # flutter_launcher_icons: ^0.9.1
overflow_view: ^0.3.1 overflow_view: ^0.3.1
flutter_styled_toast: ^2.0.0 flutter_styled_toast: ^2.0.0
permission_handler: ^9.2.0 permission_handler: ^10.2.0
file_picker: ^4.2.3 file_picker: ^5.2.5
boardview: ^0.2.2 boardview: ^0.2.2
pointer_interceptor: ^0.9.0 pointer_interceptor: ^0.9.0
contacts_service: ^0.6.1 contacts_service: ^0.6.3
diacritic: ^0.1.3 diacritic: ^0.1.3
states_rebuilder: ^5.2.0 states_rebuilder: ^6.2.0
#super_editor: ^0.2.2 # super_editor: ^0.2.2
markdown: ^5.0.0 # REMOVE THIS markdown: ^5.0.0 # REMOVE THIS
super_editor: super_editor:
git: git:
url: https://github.com/superlistapp/super_editor.git url: https://github.com/superlistapp/super_editor.git
path: super_editor path: super_editor
ref: 1601fdce95ebfa34bddf80cab3e58e700b0edec3 ref: 1601fdce95ebfa34bddf80cab3e58e700b0edec3
html2md: ^1.2.5 html2md: ^1.2.6
printing: ^5.8.0 printing: ^5.10.1
image_cropper: ^2.0.2 image_cropper: ^3.0.1
msal_js: ^2.14.0 msal_js: ^2.14.0
sign_in_with_apple: ^4.0.0 sign_in_with_apple: ^4.3.0
window_manager: ^0.2.7 window_manager: ^0.3.0
# bitsdojo_window: ^0.1.2 # bitsdojo_window: ^0.1.5
intl_phone_field: ^3.1.0 intl_phone_field: ^3.1.0
flutter_staggered_grid_view: ^0.6.2 flutter_staggered_grid_view: ^0.6.2
# quick_actions: ^0.2.1 # quick_actions: ^0.2.1

File diff suppressed because it is too large Load Diff

View File

@ -44,39 +44,39 @@ dependencies:
git: git:
url: https://github.com/theyakka/qr.flutter.git url: https://github.com/theyakka/qr.flutter.git
local_auth: ^1.1.6 local_auth: ^1.1.6
sentry_flutter: ^6.5.1 sentry_flutter: ^6.20.1
image_picker: ^0.8.3+1 image_picker: ^0.8.6+3
flutter_colorpicker: ^1.0.3 flutter_colorpicker: ^1.0.3
flutter_json_viewer: ^1.0.1 flutter_json_viewer: ^1.0.1
webview_flutter: ^2.0.10 webview_flutter: ^2.0.10
timeago: ^3.1.0 timeago: ^3.1.0
package_info: ^2.0.2 package_info: ^2.0.2
rounded_loading_button: ^2.0.5 rounded_loading_button: ^2.1.0
version: ^2.0.0 version: ^2.0.0
flutter_launcher_icons: ^0.9.1 # flutter_launcher_icons: ^0.9.1
overflow_view: ^0.3.1 overflow_view: ^0.3.1
flutter_styled_toast: ^2.0.0 flutter_styled_toast: ^2.0.0
permission_handler: ^9.2.0 permission_handler: ^9.2.0
file_picker: ^4.2.3 file_picker: ^5.2.5
boardview: ^0.2.2 boardview: ^0.2.2
pointer_interceptor: ^0.9.0 pointer_interceptor: ^0.9.0
contacts_service: ^0.6.1 contacts_service: ^0.6.3
diacritic: ^0.1.3 diacritic: ^0.1.3
states_rebuilder: ^5.2.0 states_rebuilder: ^5.2.0
#super_editor: ^0.2.0 # super_editor: ^0.2.0
markdown: ^5.0.0 # REMOVE THIS markdown: ^5.0.0 # REMOVE THIS
super_editor: super_editor:
git: git:
url: https://github.com/superlistapp/super_editor.git url: https://github.com/superlistapp/super_editor.git
path: super_editor path: super_editor
ref: 1601fdce95ebfa34bddf80cab3e58e700b0edec3 ref: 1601fdce95ebfa34bddf80cab3e58e700b0edec3
html2md: ^1.2.5 html2md: ^1.2.6
printing: ^5.8.0 printing: ^5.10.1
image_cropper: ^2.0.2 image_cropper: ^3.0.1
msal_js: ^2.14.0 msal_js: ^2.14.0
sign_in_with_apple: ^4.0.0 sign_in_with_apple: ^4.3.0
window_manager: ^0.2.5 window_manager: ^0.3.0
bitsdojo_window: ^0.1.2 # bitsdojo_window: ^0.1.5
intl_phone_field: ^3.1.0 intl_phone_field: ^3.1.0
flutter_staggered_grid_view: ^0.6.2 flutter_staggered_grid_view: ^0.6.2
# quick_actions: ^0.2.1 # quick_actions: ^0.2.1

View File

@ -20,7 +20,7 @@ dependencies:
sdk: flutter sdk: flutter
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
google_sign_in: ^5.0.7 google_sign_in: ^6.0.1
in_app_review: ^2.0.4 in_app_review: ^2.0.4
in_app_purchase: ^3.1.1 in_app_purchase: ^3.1.1
pinput: ^2.2.11 pinput: ^2.2.11
@ -29,59 +29,59 @@ dependencies:
# git: # git:
# url: https://github.com/Tkko/Flutter_Pinput # url: https://github.com/Tkko/Flutter_Pinput
# ref: dev # ref: dev
flutter_redux: ^0.8.2 flutter_redux: ^0.10.0
redux_logging: ^0.5.0 redux_logging: ^0.5.0
http: ^0.13.3 http: ^0.13.3
path_provider: ^2.0.2 path_provider: ^2.0.2
shared_preferences: ^2.0.6 shared_preferences: ^2.0.6
material_design_icons_flutter: ^5.0.6295 material_design_icons_flutter: ^6.0.7096
built_value: ^8.1.2 built_value: ^8.1.2
built_collection: ^5.1.0 built_collection: ^5.1.0
memoize: ^3.0.0 memoize: ^3.0.0
cached_network_image: 3.0.0 # imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet, cached_network_image: 3.0.0 # imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
url_launcher: ^6.0.20 url_launcher: ^6.0.20
share: ^2.0.4 share_plus: ^6.3.1
intl: ^0.17.0 intl: ^0.17.0
flutter_slidable: ^1.1.0 flutter_slidable: ^2.0.0
charts_flutter: ^0.12.0 charts_flutter: ^0.12.0
#qr_flutter: ^4.0.0 #qr_flutter: ^4.0.0
qr_flutter: # https://github.com/theyakka/qr.flutter/issues/174#issuecomment-1084235757 qr_flutter: # https://github.com/theyakka/qr.flutter/issues/174#issuecomment-1084235757
git: git:
url: https://github.com/theyakka/qr.flutter.git url: https://github.com/theyakka/qr.flutter.git
local_auth: ^1.1.6 local_auth: ^2.1.5
sentry_flutter: ^6.9.1 sentry_flutter: ^6.20.1
image_picker: ^0.8.3+1 image_picker: ^0.8.6+3
flutter_colorpicker: ^1.0.3 flutter_colorpicker: ^1.0.3
flutter_json_viewer: ^1.0.1 flutter_json_viewer: ^1.0.1
webview_flutter: ^3.0.4 webview_flutter: ^3.0.4
timeago: ^3.1.0 timeago: ^3.1.0
package_info: ^2.0.2 package_info: ^2.0.2
rounded_loading_button: ^2.0.5 rounded_loading_button: ^2.1.0
version: ^2.0.0 version: ^3.0.2
flutter_launcher_icons: ^0.9.1 # flutter_launcher_icons: ^0.9.1
overflow_view: ^0.3.1 overflow_view: ^0.3.1
flutter_styled_toast: ^2.0.0 flutter_styled_toast: ^2.0.0
permission_handler: ^9.2.0 permission_handler: ^10.2.0
file_picker: ^4.2.3 file_picker: ^5.2.5
boardview: ^0.2.2 boardview: ^0.2.2
pointer_interceptor: ^0.9.0 pointer_interceptor: ^0.9.0
contacts_service: ^0.6.1 contacts_service: ^0.6.3
diacritic: ^0.1.3 diacritic: ^0.1.3
states_rebuilder: ^5.2.0 states_rebuilder: ^6.2.0
#super_editor: ^0.2.2 # super_editor: ^0.2.2
markdown: ^5.0.0 # REMOVE THIS markdown: ^5.0.0 # REMOVE THIS
super_editor: super_editor:
git: git:
url: https://github.com/superlistapp/super_editor.git url: https://github.com/superlistapp/super_editor.git
path: super_editor path: super_editor
ref: 1601fdce95ebfa34bddf80cab3e58e700b0edec3 #ref: 1601fdce95ebfa34bddf80cab3e58e700b0edec3
html2md: ^1.2.5 html2md: ^1.2.6
printing: ^5.8.0 printing: ^5.10.1
image_cropper: ^2.0.2 image_cropper: ^3.0.1
msal_js: ^2.14.0 msal_js: ^2.14.0
sign_in_with_apple: ^4.0.0 sign_in_with_apple: ^4.3.0
window_manager: ^0.2.7 window_manager: ^0.3.0
# bitsdojo_window: ^0.1.2 # bitsdojo_window: ^0.1.5
intl_phone_field: ^3.1.0 intl_phone_field: ^3.1.0
flutter_staggered_grid_view: ^0.6.2 flutter_staggered_grid_view: ^0.6.2
# quick_actions: ^0.2.1 # quick_actions: ^0.2.1

View File

@ -6,15 +6,19 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <local_auth_windows/local_auth_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <printing/printing_plugin.h> #include <printing/printing_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h> #include <screen_retriever/screen_retriever_plugin.h>
#include <sentry_flutter/sentry_flutter_plugin.h> #include <sentry_flutter/sentry_flutter_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <smart_auth/smart_auth_plugin.h> #include <smart_auth/smart_auth_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h> #include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
LocalAuthPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar( PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
PrintingPluginRegisterWithRegistrar( PrintingPluginRegisterWithRegistrar(
@ -23,6 +27,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
SentryFlutterPluginRegisterWithRegistrar( SentryFlutterPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SentryFlutterPlugin")); registry->GetRegistrarForPlugin("SentryFlutterPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
SmartAuthPluginRegisterWithRegistrar( SmartAuthPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SmartAuthPlugin")); registry->GetRegistrarForPlugin("SmartAuthPlugin"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(

View File

@ -3,10 +3,12 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
local_auth_windows
permission_handler_windows permission_handler_windows
printing printing
screen_retriever screen_retriever
sentry_flutter sentry_flutter
share_plus
smart_auth smart_auth
url_launcher_windows url_launcher_windows
window_manager window_manager

View File

@ -20,12 +20,20 @@ add_executable(${BINARY_NAME} WIN32
# that need different build settings. # that need different build settings.
apply_standard_settings(${BINARY_NAME}) apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the build version.
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
# Disable Windows macros that collide with C++ standard library functions. # Disable Windows macros that collide with C++ standard library functions.
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
# Add dependency libraries and include directories. Add any application-specific # Add dependency libraries and include directories. Add any application-specific
# dependencies here. # dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
# Run the Flutter tool portions of the build. This must not be removed. # Run the Flutter tool portions of the build. This must not be removed.

View File

@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico"
// Version // Version
// //
#ifdef FLUTTER_BUILD_NUMBER #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
#else #else
#define VERSION_AS_NUMBER 1,0,0 #define VERSION_AS_NUMBER 1,0,0,0
#endif #endif
#ifdef FLUTTER_BUILD_NAME #if defined(FLUTTER_VERSION)
#define VERSION_AS_STRING #FLUTTER_BUILD_NAME #define VERSION_AS_STRING FLUTTER_VERSION
#else #else
#define VERSION_AS_STRING "1.0.0" #define VERSION_AS_STRING "1.0.0"
#endif #endif
@ -93,7 +93,7 @@ BEGIN
VALUE "FileDescription", "Invoice Ninja" "\0" VALUE "FileDescription", "Invoice Ninja" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "invoiceninja" "\0" VALUE "InternalName", "invoiceninja" "\0"
VALUE "LegalCopyright", "Copyright (C) 2021 Invoice Ninja. All rights reserved." "\0" VALUE "LegalCopyright", "Copyright (C) 2023 Invoice Ninja. All rights reserved." "\0"
VALUE "OriginalFilename", "invoiceninja.exe" "\0" VALUE "OriginalFilename", "invoiceninja.exe" "\0"
VALUE "ProductName", "invoiceninja" "\0" VALUE "ProductName", "invoiceninja" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0"

View File

@ -26,6 +26,11 @@ bool FlutterWindow::OnCreate() {
} }
RegisterPlugins(flutter_controller_->engine()); RegisterPlugins(flutter_controller_->engine());
SetChildContent(flutter_controller_->view()->GetNativeWindow()); SetChildContent(flutter_controller_->view()->GetNativeWindow());
flutter_controller_->engine()->SetNextFrameCallback([&]() {
this->Show();
});
return true; return true;
} }

View File

@ -1,6 +1,3 @@
//#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
//auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP);
#include <flutter/dart_project.h> #include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h> #include <flutter/flutter_view_controller.h>
#include <windows.h> #include <windows.h>
@ -30,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
FlutterWindow window(project); FlutterWindow window(project);
Win32Window::Point origin(10, 10); Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720); Win32Window::Size size(1280, 720);
if (!window.CreateAndShow(L"Invoice Ninja", origin, size)) { if (!window.Create(L"Invoice Ninja", origin, size)) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
window.SetQuitOnClose(true); window.SetQuitOnClose(true);

View File

@ -7,7 +7,7 @@
</application> </application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application> <application>
<!-- Windows 10 --> <!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<!-- Windows 8.1 --> <!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>

View File

@ -1,13 +1,31 @@
#include "win32_window.h" #include "win32_window.h"
#include <dwmapi.h>
#include <flutter_windows.h> #include <flutter_windows.h>
#include "resource.h" #include "resource.h"
namespace { namespace {
/// Window attribute that enables dark mode window decorations.
///
/// Redefined in case the developer's machine has a Windows SDK older than
/// version 10.0.22000.0.
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
/// Registry key for app theme preference.
///
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
/// value indicates apps should use light mode.
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
// The number of Win32Window objects that currently exist. // The number of Win32Window objects that currently exist.
static int g_active_window_count = 0; static int g_active_window_count = 0;
@ -31,8 +49,8 @@ void EnableFullDpiSupportIfAvailable(HWND hwnd) {
GetProcAddress(user32_module, "EnableNonClientDpiScaling")); GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
if (enable_non_client_dpi_scaling != nullptr) { if (enable_non_client_dpi_scaling != nullptr) {
enable_non_client_dpi_scaling(hwnd); enable_non_client_dpi_scaling(hwnd);
FreeLibrary(user32_module);
} }
FreeLibrary(user32_module);
} }
} // namespace } // namespace
@ -102,9 +120,9 @@ Win32Window::~Win32Window() {
Destroy(); Destroy();
} }
bool Win32Window::CreateAndShow(const std::wstring& title, bool Win32Window::Create(const std::wstring& title,
const Point& origin, const Point& origin,
const Size& size) { const Size& size) {
Destroy(); Destroy();
const wchar_t* window_class = const wchar_t* window_class =
@ -117,7 +135,7 @@ bool Win32Window::CreateAndShow(const std::wstring& title,
double scale_factor = dpi / 96.0; double scale_factor = dpi / 96.0;
HWND window = CreateWindow( HWND window = CreateWindow(
window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
Scale(size.width, scale_factor), Scale(size.height, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor),
nullptr, nullptr, GetModuleHandle(nullptr), this); nullptr, nullptr, GetModuleHandle(nullptr), this);
@ -126,9 +144,15 @@ bool Win32Window::CreateAndShow(const std::wstring& title,
return false; return false;
} }
UpdateTheme(window);
return OnCreate(); return OnCreate();
} }
bool Win32Window::Show() {
return ShowWindow(window_handle_, SW_SHOWNORMAL);
}
// static // static
LRESULT CALLBACK Win32Window::WndProc(HWND const window, LRESULT CALLBACK Win32Window::WndProc(HWND const window,
UINT const message, UINT const message,
@ -188,6 +212,10 @@ Win32Window::MessageHandler(HWND hwnd,
SetFocus(child_content_); SetFocus(child_content_);
} }
return 0; return 0;
case WM_DWMCOLORIZATIONCOLORCHANGED:
UpdateTheme(hwnd);
return 0;
} }
return DefWindowProc(window_handle_, message, wparam, lparam); return DefWindowProc(window_handle_, message, wparam, lparam);
@ -243,3 +271,18 @@ bool Win32Window::OnCreate() {
void Win32Window::OnDestroy() { void Win32Window::OnDestroy() {
// No-op; provided for subclasses. // No-op; provided for subclasses.
} }
void Win32Window::UpdateTheme(HWND const window) {
DWORD light_mode;
DWORD light_mode_size = sizeof(light_mode);
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
kGetPreferredBrightnessRegValue,
RRF_RT_REG_DWORD, nullptr, &light_mode,
&light_mode_size);
if (result == ERROR_SUCCESS) {
BOOL enable_dark_mode = light_mode == 0;
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
&enable_dark_mode, sizeof(enable_dark_mode));
}
}

View File

@ -28,15 +28,16 @@ class Win32Window {
Win32Window(); Win32Window();
virtual ~Win32Window(); virtual ~Win32Window();
// Creates and shows a win32 window with |title| and position and size using // Creates a win32 window with |title| that is positioned and sized using
// |origin| and |size|. New windows are created on the default monitor. Window // |origin| and |size|. New windows are created on the default monitor. Window
// sizes are specified to the OS in physical pixels, hence to ensure a // sizes are specified to the OS in physical pixels, hence to ensure a
// consistent size to will treat the width height passed in to this function // consistent size this function will scale the inputted width and height as
// as logical pixels and scale to appropriate for the default monitor. Returns // as appropriate for the default monitor. The window is invisible until
// true if the window was created successfully. // |Show| is called. Returns true if the window was created successfully.
bool CreateAndShow(const std::wstring& title, bool Create(const std::wstring& title, const Point& origin, const Size& size);
const Point& origin,
const Size& size); // Show the current window. Returns true if the window was successfully shown.
bool Show();
// Release OS resources associated with window. // Release OS resources associated with window.
void Destroy(); void Destroy();
@ -86,6 +87,9 @@ class Win32Window {
// Retrieves a class instance pointer for |window| // Retrieves a class instance pointer for |window|
static Win32Window* GetThisFromHandle(HWND const window) noexcept; static Win32Window* GetThisFromHandle(HWND const window) noexcept;
// Update the window frame's theme to match the system theme.
static void UpdateTheme(HWND const window);
bool quit_on_close_ = false; bool quit_on_close_ = false;
// window handle for top level window. // window handle for top level window.