Merge branch 'develop'
This commit is contained in:
commit
8238404da1
|
|
@ -86,7 +86,7 @@ jobs:
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
title: "Latest Release"
|
title: "Latest Release"
|
||||||
automatic_release_tag: "v5.0.146"
|
automatic_release_tag: "v5.0.147"
|
||||||
files: |
|
files: |
|
||||||
${{ github.workspace }}/artifacts/Invoice-Ninja-Archive
|
${{ github.workspace }}/artifacts/Invoice-Ninja-Archive
|
||||||
${{ github.workspace }}/artifacts/Invoice-Ninja-Hash
|
${{ github.workspace }}/artifacts/Invoice-Ninja-Hash
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@
|
||||||
</screenshots>
|
</screenshots>
|
||||||
<content_rating type="oars-1.1"/>
|
<content_rating type="oars-1.1"/>
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="5.0.147" date="2024-01-02"/>
|
||||||
<release version="5.0.146" date="2023-12-20"/>
|
<release version="5.0.146" date="2023-12-20"/>
|
||||||
<release version="5.0.145" date="2023-12-03"/>
|
<release version="5.0.145" date="2023-12-03"/>
|
||||||
<release version="5.0.144" date="2023-12-01"/>
|
<release version="5.0.144" date="2023-12-01"/>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ class Constants {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO remove version once #46609 is fixed
|
// TODO remove version once #46609 is fixed
|
||||||
const String kClientVersion = '5.0.146';
|
const String kClientVersion = '5.0.147';
|
||||||
const String kMinServerVersion = '5.0.4';
|
const String kMinServerVersion = '5.0.4';
|
||||||
|
|
||||||
const String kAppName = 'Invoice Ninja';
|
const String kAppName = 'Invoice Ninja';
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,8 @@ class ProjectRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<BuiltList<ProjectEntity>> loadList(
|
Future<BuiltList<ProjectEntity>> loadList(
|
||||||
Credentials credentials, int createdAt, bool filterDeleted) async {
|
Credentials credentials, bool filterDeleted) async {
|
||||||
String url = credentials.url+ '/projects?created_at=$createdAt';
|
String url = credentials.url + '/projects?';
|
||||||
|
|
||||||
if (filterDeleted) {
|
if (filterDeleted) {
|
||||||
url += '&filter_deleted_clients=true';
|
url += '&filter_deleted_clients=true';
|
||||||
|
|
@ -60,7 +60,7 @@ class ProjectRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
final url =
|
final url =
|
||||||
credentials.url+ '/projects/bulk?per_page=$kMaxEntitiesPerBulkAction';
|
credentials.url + '/projects/bulk?per_page=$kMaxEntitiesPerBulkAction';
|
||||||
final dynamic response = await webClient.post(url, credentials.token,
|
final dynamic response = await webClient.post(url, credentials.token,
|
||||||
data: json.encode({'ids': ids, 'action': action.toApiParam()}));
|
data: json.encode({'ids': ids, 'action': action.toApiParam()}));
|
||||||
|
|
||||||
|
|
@ -77,10 +77,10 @@ class ProjectRepository {
|
||||||
|
|
||||||
if (project.isNew) {
|
if (project.isNew) {
|
||||||
response = await webClient.post(
|
response = await webClient.post(
|
||||||
credentials.url+ '/projects', credentials.token,
|
credentials.url + '/projects', credentials.token,
|
||||||
data: json.encode(data));
|
data: json.encode(data));
|
||||||
} else {
|
} else {
|
||||||
final url = credentials.url+ '/projects/${project.id}';
|
final url = credentials.url + '/projects/${project.id}';
|
||||||
response =
|
response =
|
||||||
await webClient.put(url, credentials.token, data: json.encode(data));
|
await webClient.put(url, credentials.token, data: json.encode(data));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,6 @@ Middleware<AppState> _loadProjects(ProjectRepository repository) {
|
||||||
repository
|
repository
|
||||||
.loadList(
|
.loadList(
|
||||||
state.credentials,
|
state.credentials,
|
||||||
state.createdAtLimit,
|
|
||||||
state.filterDeletedClients,
|
state.filterDeletedClients,
|
||||||
)
|
)
|
||||||
.then((data) {
|
.then((data) {
|
||||||
|
|
|
||||||
|
|
@ -297,24 +297,18 @@ class _InvoiceEmailViewState extends State<InvoiceEmailView>
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
child: Stack(
|
child: (supportsInlineBrowser())
|
||||||
children: [
|
? EmailPreview(
|
||||||
if (_isLoading) LinearProgressIndicator(),
|
|
||||||
if (supportsInlineBrowser())
|
|
||||||
EmailPreview(
|
|
||||||
isLoading: _isLoading,
|
isLoading: _isLoading,
|
||||||
subject: _subjectPreview,
|
subject: _subjectPreview,
|
||||||
body: _emailPreview,
|
body: _emailPreview,
|
||||||
)
|
)
|
||||||
else
|
: IgnorePointer(
|
||||||
IgnorePointer(
|
|
||||||
child: ExampleEditor(
|
child: ExampleEditor(
|
||||||
value: '### $_subjectPreview\n\n\n' +
|
value: '### $_subjectPreview\n\n\n' +
|
||||||
html2md.convert(_bodyPreview),
|
html2md.convert(_bodyPreview),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -372,36 +366,41 @@ class _InvoiceEmailViewState extends State<InvoiceEmailView>
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (state.company.markdownEmailEnabled)
|
Expanded(
|
||||||
Expanded(
|
child: Stack(
|
||||||
child: ColoredBox(
|
children: [
|
||||||
color: Colors.white,
|
if (state.company.markdownEmailEnabled)
|
||||||
child: IgnorePointer(
|
ColoredBox(
|
||||||
ignoring: !enableCustomEmail,
|
color: Colors.white,
|
||||||
child: ExampleEditor(
|
child: IgnorePointer(
|
||||||
value: _rawBodyPreview,
|
ignoring: !enableCustomEmail,
|
||||||
onChanged: (value) {
|
child: ExampleEditor(
|
||||||
if (value.trim() != _bodyController.text.trim()) {
|
value: _rawBodyPreview,
|
||||||
_bodyController.text = value;
|
onChanged: (value) {
|
||||||
_onChanged();
|
if (value.trim() != _bodyController.text.trim()) {
|
||||||
}
|
_bodyController.text = value;
|
||||||
},
|
_onChanged();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: DecoratedFormField(
|
||||||
|
controller: _bodyController,
|
||||||
|
label: localization.body,
|
||||||
|
maxLines: enableCustomEmail ? 6 : 2,
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
onChanged: (_) => _onChanged(),
|
||||||
|
enabled: enableCustomEmail,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
if (_isLoading) LinearProgressIndicator(),
|
||||||
),
|
],
|
||||||
)
|
),
|
||||||
else
|
),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
child: DecoratedFormField(
|
|
||||||
controller: _bodyController,
|
|
||||||
label: localization.body,
|
|
||||||
maxLines: enableCustomEmail ? 6 : 2,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
onChanged: (_) => _onChanged(),
|
|
||||||
enabled: enableCustomEmail,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ class _MenuDrawerState extends State<MenuDrawer> {
|
||||||
// Fix for CORS error using 'object' subdomain
|
// Fix for CORS error using 'object' subdomain
|
||||||
return CachedImage(
|
return CachedImage(
|
||||||
width: MenuDrawer.LOGO_WIDTH,
|
width: MenuDrawer.LOGO_WIDTH,
|
||||||
url: state.credentials.url+ '/companies/' + company.id + '/logo',
|
url: state.credentials.url + '/companies/' + company.id + '/logo',
|
||||||
apiToken: state.userCompanyStates
|
apiToken: state.userCompanyStates
|
||||||
.firstWhere((userCompanyState) =>
|
.firstWhere((userCompanyState) =>
|
||||||
userCompanyState.company.id == company.id)
|
userCompanyState.company.id == company.id)
|
||||||
|
|
@ -967,9 +967,14 @@ class _DrawerTileState extends State<DrawerTile> {
|
||||||
? iconWidget
|
? iconWidget
|
||||||
: isLoading
|
: isLoading
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.only(left: 10),
|
padding: const EdgeInsets.only(
|
||||||
|
left: 10,
|
||||||
|
right: 8,
|
||||||
|
),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(
|
||||||
|
color: state.accentColor,
|
||||||
|
),
|
||||||
width: 22,
|
width: 22,
|
||||||
height: 22,
|
height: 22,
|
||||||
),
|
),
|
||||||
|
|
@ -1680,7 +1685,7 @@ class _ContactUsDialogState extends State<ContactUsDialog> {
|
||||||
|
|
||||||
setState(() => _isSaving = true);
|
setState(() => _isSaving = true);
|
||||||
WebClient()
|
WebClient()
|
||||||
.post(state.credentials.url+ '/support/messages/send',
|
.post(state.credentials.url + '/support/messages/send',
|
||||||
state.credentials.token,
|
state.credentials.token,
|
||||||
data: json.encode({
|
data: json.encode({
|
||||||
'message': _message,
|
'message': _message,
|
||||||
|
|
|
||||||
|
|
@ -268,6 +268,11 @@ class InvoiceOverview extends StatelessWidget {
|
||||||
widgets.add(EntityListTile(entity: project, isFilter: isFilter));
|
widgets.add(EntityListTile(entity: project, isFilter: isFilter));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((invoice.invoiceId ?? '').isNotEmpty) {
|
||||||
|
final linkedInvoice = state.invoiceState.get(invoice.invoiceId!);
|
||||||
|
widgets.add(EntityListTile(entity: linkedInvoice, isFilter: isFilter));
|
||||||
|
}
|
||||||
|
|
||||||
if (invoice.expenseId.isNotEmpty) {
|
if (invoice.expenseId.isNotEmpty) {
|
||||||
final expense = state.vendorState.get(invoice.expenseId);
|
final expense = state.vendorState.get(invoice.expenseId);
|
||||||
widgets.add(EntityListTile(entity: expense, isFilter: isFilter));
|
widgets.add(EntityListTile(entity: expense, isFilter: isFilter));
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,75 @@ MutableDocument deserializeMarkdownToDocument(String markdown) {
|
||||||
return MutableDocument(nodes: nodeVisitor.content);
|
return MutableDocument(nodes: nodeVisitor.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String serializeDocumentToMarkdown(Document doc) {
|
||||||
|
final StringBuffer buffer = StringBuffer();
|
||||||
|
|
||||||
|
bool isFirstLine = true;
|
||||||
|
for (int i = 0; i < doc.nodes.length; ++i) {
|
||||||
|
final node = doc.nodes[i];
|
||||||
|
|
||||||
|
if (!isFirstLine) {
|
||||||
|
// Create a new line to encode the given node.
|
||||||
|
buffer.writeln('');
|
||||||
|
} else {
|
||||||
|
isFirstLine = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node is ImageNode) {
|
||||||
|
buffer.write('');
|
||||||
|
} else if (node is HorizontalRuleNode) {
|
||||||
|
buffer.write('---');
|
||||||
|
} else if (node is ListItemNode) {
|
||||||
|
final indent = List.generate(node.indent + 1, (index) => ' ').join('');
|
||||||
|
final symbol = node.type == ListItemType.unordered ? '*' : '1.';
|
||||||
|
|
||||||
|
buffer.write('$indent$symbol ${node.text.toMarkdown()}');
|
||||||
|
|
||||||
|
final nodeBelow = i < doc.nodes.length - 1 ? doc.nodes[i + 1] : null;
|
||||||
|
if (nodeBelow != null && (nodeBelow is! ListItemNode)) {
|
||||||
|
// This list item is the last item in the list. Add an extra
|
||||||
|
// blank line after it.
|
||||||
|
buffer.writeln('');
|
||||||
|
}
|
||||||
|
} else if (node is ParagraphNode) {
|
||||||
|
final Attribution blockType = node.getMetadataValue('blockType');
|
||||||
|
|
||||||
|
if (blockType == header1Attribution) {
|
||||||
|
buffer.write('# ${node.text.toMarkdown()}');
|
||||||
|
} else if (blockType == header2Attribution) {
|
||||||
|
buffer.write('## ${node.text.toMarkdown()}');
|
||||||
|
} else if (blockType == header3Attribution) {
|
||||||
|
buffer.write('### ${node.text.toMarkdown()}');
|
||||||
|
} else if (blockType == header4Attribution) {
|
||||||
|
buffer.write('#### ${node.text.toMarkdown()}');
|
||||||
|
} else if (blockType == header5Attribution) {
|
||||||
|
buffer.write('##### ${node.text.toMarkdown()}');
|
||||||
|
} else if (blockType == header6Attribution) {
|
||||||
|
buffer.write('###### ${node.text.toMarkdown()}');
|
||||||
|
} else if (blockType == blockquoteAttribution) {
|
||||||
|
// TODO: handle multiline
|
||||||
|
buffer.write('> ${node.text.toMarkdown()}');
|
||||||
|
} else if (blockType == codeAttribution) {
|
||||||
|
buffer //
|
||||||
|
..writeln('```') //
|
||||||
|
..writeln(node.text.toMarkdown()) //
|
||||||
|
..write('```');
|
||||||
|
} else {
|
||||||
|
buffer.write(node.text.toMarkdown());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separates paragraphs with blank lines.
|
||||||
|
// If we are at the last node we don't add a trailing
|
||||||
|
// blank line.
|
||||||
|
if (i != doc.nodes.length - 1) {
|
||||||
|
buffer.writeln();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
/// Converts structured markdown to a list of [DocumentNode]s.
|
/// Converts structured markdown to a list of [DocumentNode]s.
|
||||||
///
|
///
|
||||||
/// To use [_MarkdownToDocument], obtain a series of markdown
|
/// To use [_MarkdownToDocument], obtain a series of markdown
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ class _ExampleEditorState extends State<ExampleEditor> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
// Fix for <p> tags cutting off text
|
// Fix for <p> tags cutting off text
|
||||||
var markdown = widget.value;
|
var markdown = widget.value;
|
||||||
markdown = markdown.replaceAll('<p/>', '\n');
|
markdown = markdown.replaceAll('<p/>', '\n');
|
||||||
|
|
@ -71,7 +72,6 @@ class _ExampleEditorState extends State<ExampleEditor> {
|
||||||
markdown = markdown.replaceAll('</p>', '');
|
markdown = markdown.replaceAll('</p>', '');
|
||||||
markdown = markdown.replaceAll('</div>', '');
|
markdown = markdown.replaceAll('</div>', '');
|
||||||
|
|
||||||
// _doc = createInitialDocument()..addListener(_onDocumentChange);
|
|
||||||
_doc = deserializeMarkdownToDocument(markdown)
|
_doc = deserializeMarkdownToDocument(markdown)
|
||||||
..addListener(_onDocumentChange);
|
..addListener(_onDocumentChange);
|
||||||
_composer = MutableDocumentComposer();
|
_composer = MutableDocumentComposer();
|
||||||
|
|
@ -91,8 +91,35 @@ class _ExampleEditorState extends State<ExampleEditor> {
|
||||||
_iosControlsController = SuperEditorIosControlsController();
|
_iosControlsController = SuperEditorIosControlsController();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(ExampleEditor oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
|
if (widget.value != oldWidget.value) {
|
||||||
|
_setValue(widget.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setValue(String value) {
|
||||||
|
// Fix for <p> tags cutting off text
|
||||||
|
var markdown = widget.value;
|
||||||
|
markdown = markdown.replaceAll('<p/>', '\n');
|
||||||
|
markdown = markdown.replaceAll('<p>', '\n');
|
||||||
|
markdown = markdown.replaceAll('<div>', '\n');
|
||||||
|
markdown = markdown.replaceAll('</p>', '');
|
||||||
|
markdown = markdown.replaceAll('</div>', '');
|
||||||
|
|
||||||
|
_doc.removeListener(_onDocumentChange);
|
||||||
|
_doc = deserializeMarkdownToDocument(markdown)
|
||||||
|
..addListener(_onDocumentChange);
|
||||||
|
|
||||||
|
_docEditor =
|
||||||
|
createDefaultDocumentEditor(document: _doc, composer: _composer);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_doc.removeListener(_onDocumentChange);
|
||||||
_iosControlsController.dispose();
|
_iosControlsController.dispose();
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
_editorFocusNode.dispose();
|
_editorFocusNode.dispose();
|
||||||
|
|
@ -103,6 +130,11 @@ class _ExampleEditorState extends State<ExampleEditor> {
|
||||||
void _onDocumentChange(_) {
|
void _onDocumentChange(_) {
|
||||||
_hideOrShowToolbar();
|
_hideOrShowToolbar();
|
||||||
_docChangeSignal.notifyListeners();
|
_docChangeSignal.notifyListeners();
|
||||||
|
|
||||||
|
if (widget.onChanged != null) {
|
||||||
|
final value = serializeDocumentToMarkdown(_doc);
|
||||||
|
widget.onChanged!(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _hideOrShowToolbar() {
|
void _hideOrShowToolbar() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,416 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:super_editor/super_editor.dart';
|
||||||
|
|
||||||
|
/// A selection control, which displays a button with the selected item, and upon tap, displays a
|
||||||
|
/// popover list of available text options, from which the user can select a different
|
||||||
|
/// option.
|
||||||
|
///
|
||||||
|
/// Unlike Flutter `DropdownButton`, which displays the popover list in a separate route,
|
||||||
|
/// this widget displays its popover list in an `Overlay`. By using an `Overlay`, focus can be shared
|
||||||
|
/// with the [parentFocusNode]. This means that when the popover list requests focus, [parentFocusNode]
|
||||||
|
/// still has non-primary focus.
|
||||||
|
///
|
||||||
|
/// The popover list is positioned based on the following rules:
|
||||||
|
///
|
||||||
|
/// 1. The popover is displayed below the selected item, if there's enough room, or
|
||||||
|
/// 2. The popover is displayed above the selected item, if there's enough room, or
|
||||||
|
/// 3. The popover is displayed with its bottom aligned with the bottom of
|
||||||
|
/// the given boundary, and it covers the selected item.
|
||||||
|
///
|
||||||
|
/// The popover list height is based on the following rules:
|
||||||
|
///
|
||||||
|
/// 1. The popover is displayed as tall as all items in the list, if there's enough room, or
|
||||||
|
/// 2. The popover is displayed as tall as the available space and becomes scrollable.
|
||||||
|
///
|
||||||
|
/// The popover list includes keyboard selection behaviors:
|
||||||
|
///
|
||||||
|
/// * Pressing UP/DOWN moves the "active" item selection up/down.
|
||||||
|
/// * Pressing UP with the first item active moves the active item selection to the last item.
|
||||||
|
/// * Pressing DOWN with the last item active moves the active item selection to the first item.
|
||||||
|
/// * Pressing ENTER selects the currently active item and closes the popover list.
|
||||||
|
class SuperEditorDemoTextItemSelector extends StatefulWidget {
|
||||||
|
const SuperEditorDemoTextItemSelector({
|
||||||
|
super.key,
|
||||||
|
this.parentFocusNode,
|
||||||
|
this.boundaryKey,
|
||||||
|
this.id,
|
||||||
|
required this.items,
|
||||||
|
required this.onSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The [FocusNode], to which the popover list's [FocusNode] will be added as a child.
|
||||||
|
///
|
||||||
|
/// In Flutter, [FocusNode]s have parents and children. This relationship allows an
|
||||||
|
/// entire ancestor path to "have focus", but only the lowest level descendant
|
||||||
|
/// in that path has "primary focus". This path is important because various
|
||||||
|
/// widgets alter their presentation or behavior based on whether or not they
|
||||||
|
/// currently have focus, even if they only have "non-primary focus".
|
||||||
|
///
|
||||||
|
/// When the popover list of items is visible, that list will have primary focus.
|
||||||
|
/// Moreover, because the popover list is built in an `Overlay`, none of your
|
||||||
|
/// widgets are in the natural focus path for that popover list. Therefore, if you
|
||||||
|
/// need your widget tree to retain focus while the popover list is visible, then
|
||||||
|
/// you need to provide the [FocusNode] that the popover list should use as its
|
||||||
|
/// parent, thereby retaining focus for your widgets.
|
||||||
|
final FocusNode? parentFocusNode;
|
||||||
|
|
||||||
|
/// A [GlobalKey] to a widget that determines the bounds where the popover list can be displayed.
|
||||||
|
///
|
||||||
|
/// As the popover list follows the selected item, it can be displayed off-screen if this [SuperEditorDemoTextItemSelector]
|
||||||
|
/// is close to the bottom of the screen.
|
||||||
|
///
|
||||||
|
/// Passing a [boundaryKey] causes the popover list to be confined to the bounds of the widget
|
||||||
|
/// bound to the [boundaryKey].
|
||||||
|
///
|
||||||
|
/// If `null`, the popover list is confined to the screen bounds, defined by the result of `MediaQuery.sizeOf`.
|
||||||
|
final GlobalKey? boundaryKey;
|
||||||
|
|
||||||
|
/// The currently selected value or `null` if no item is selected.
|
||||||
|
///
|
||||||
|
/// This value is used to build the button.
|
||||||
|
final SuperEditorDemoTextItem? id;
|
||||||
|
|
||||||
|
/// The items that will be displayed in the popover list.
|
||||||
|
///
|
||||||
|
/// For each item, its [SuperEditorDemoTextItem.label] is displayed.
|
||||||
|
final List<SuperEditorDemoTextItem> items;
|
||||||
|
|
||||||
|
/// Called when the user selects an item on the popover list.
|
||||||
|
final void Function(SuperEditorDemoTextItem? value) onSelected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SuperEditorDemoTextItemSelector> createState() =>
|
||||||
|
_SuperEditorDemoTextItemSelectorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SuperEditorDemoTextItemSelectorState
|
||||||
|
extends State<SuperEditorDemoTextItemSelector> {
|
||||||
|
/// Shows and hides the popover.
|
||||||
|
final PopoverController _popoverController = PopoverController();
|
||||||
|
|
||||||
|
/// The [FocusNode] of the popover list.
|
||||||
|
final FocusNode _popoverFocusNode = FocusNode();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_popoverController.dispose();
|
||||||
|
_popoverFocusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onItemSelected(SuperEditorDemoTextItem? value) {
|
||||||
|
_popoverController.close();
|
||||||
|
widget.onSelected(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PopoverScaffold(
|
||||||
|
controller: _popoverController,
|
||||||
|
buttonBuilder: _buildButton,
|
||||||
|
popoverFocusNode: _popoverFocusNode,
|
||||||
|
parentFocusNode: widget.parentFocusNode,
|
||||||
|
boundaryKey: widget.boundaryKey,
|
||||||
|
popoverBuilder: (context) => RoundedRectanglePopoverAppearance(
|
||||||
|
child: ItemSelectionList<SuperEditorDemoTextItem>(
|
||||||
|
focusNode: _popoverFocusNode,
|
||||||
|
value: widget.id,
|
||||||
|
items: widget.items,
|
||||||
|
itemBuilder: _buildPopoverListItem,
|
||||||
|
onItemSelected: _onItemSelected,
|
||||||
|
onCancel: () => _popoverController.close(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildButton(BuildContext context) {
|
||||||
|
return SuperEditorPopoverButton(
|
||||||
|
padding: const EdgeInsets.only(left: 16.0, right: 24),
|
||||||
|
onTap: () => _popoverController.open(),
|
||||||
|
child: widget.id == null //
|
||||||
|
? const SizedBox()
|
||||||
|
: Text(
|
||||||
|
widget.id!.label,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPopoverListItem(BuildContext context,
|
||||||
|
SuperEditorDemoTextItem item, bool isActive, VoidCallback onTap) {
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isActive ? Colors.grey.withOpacity(0.2) : Colors.transparent,
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
constraints:
|
||||||
|
const BoxConstraints(minHeight: kMinInteractiveDimension),
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
|
child: Text(
|
||||||
|
item.label,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An option that is displayed as text by a [SuperEditorDemoTextItemSelector].
|
||||||
|
///
|
||||||
|
/// Two [SuperEditorDemoTextItem]s are considered to be equal if they have the same [id].
|
||||||
|
class SuperEditorDemoTextItem {
|
||||||
|
const SuperEditorDemoTextItem({
|
||||||
|
required this.id,
|
||||||
|
required this.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The value that identifies this item.
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// The text that is displayed.
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is SuperEditorDemoTextItem &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
id == other.id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => id.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A selection control, which displays a button with the selected item, and upon tap, displays a
|
||||||
|
/// popover list of available icons, from which the user can select a different option.
|
||||||
|
///
|
||||||
|
/// Unlike Flutter `DropdownButton`, which displays the popover list in a separate route,
|
||||||
|
/// this widget displays its popover list in an `Overlay`. By using an `Overlay`, focus can be shared
|
||||||
|
/// with the [parentFocusNode]. This means that when the popover list requests focus, [parentFocusNode]
|
||||||
|
/// still has non-primary focus.
|
||||||
|
///
|
||||||
|
/// The popover list is positioned based on the following rules:
|
||||||
|
///
|
||||||
|
/// 1. The popover is displayed below the selected item, if there's enough room, or
|
||||||
|
/// 2. The popover is displayed above the selected item, if there's enough room, or
|
||||||
|
/// 3. The popover is displayed with its bottom aligned with the bottom of
|
||||||
|
/// the given boundary, and it covers the selected item.
|
||||||
|
///
|
||||||
|
/// The popover list height is based on the following rules:
|
||||||
|
///
|
||||||
|
/// 1. The popover is displayed as tall as all items in the list, if there's enough room, or
|
||||||
|
/// 2. The popover is displayed as tall as the available space and becomes scrollable.
|
||||||
|
///
|
||||||
|
/// The popover list includes keyboard selection behaviors:
|
||||||
|
///
|
||||||
|
/// * Pressing UP/DOWN moves the "active" item selection up/down.
|
||||||
|
/// * Pressing UP with the first item active moves the active item selection to the last item.
|
||||||
|
/// * Pressing DOWN with the last item active moves the active item selection to the first item.
|
||||||
|
/// * Pressing ENTER selects the currently active item and closes the popover list.
|
||||||
|
class SuperEditorDemoIconItemSelector extends StatefulWidget {
|
||||||
|
const SuperEditorDemoIconItemSelector({
|
||||||
|
super.key,
|
||||||
|
this.parentFocusNode,
|
||||||
|
this.boundaryKey,
|
||||||
|
this.value,
|
||||||
|
required this.items,
|
||||||
|
required this.onSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The [FocusNode], to which the popover list's [FocusNode] will be added as a child.
|
||||||
|
///
|
||||||
|
/// In Flutter, [FocusNode]s have parents and children. This relationship allows an
|
||||||
|
/// entire ancestor path to "have focus", but only the lowest level descendant
|
||||||
|
/// in that path has "primary focus". This path is important because various
|
||||||
|
/// widgets alter their presentation or behavior based on whether or not they
|
||||||
|
/// currently have focus, even if they only have "non-primary focus".
|
||||||
|
///
|
||||||
|
/// When the popover list of items is visible, that list will have primary focus.
|
||||||
|
/// Moreover, because the popover list is built in an `Overlay`, none of your
|
||||||
|
/// widgets are in the natural focus path for that popover list. Therefore, if you
|
||||||
|
/// need your widget tree to retain focus while the popover list is visible, then
|
||||||
|
/// you need to provide the [FocusNode] that the popover list should use as its
|
||||||
|
/// parent, thereby retaining focus for your widgets.
|
||||||
|
final FocusNode? parentFocusNode;
|
||||||
|
|
||||||
|
/// A [GlobalKey] to a widget that determines the bounds where the popover list can be displayed.
|
||||||
|
///
|
||||||
|
/// As the popover list follows the selected item, it can be displayed off-screen if this [SuperEditorDemoIconItemSelector]
|
||||||
|
/// is close to the bottom of the screen.
|
||||||
|
///
|
||||||
|
/// Passing a [boundaryKey] causes the popover list to be confined to the bounds of the widget
|
||||||
|
/// bound to the [boundaryKey].
|
||||||
|
///
|
||||||
|
/// If `null`, the popover list is confined to the screen bounds, defined by the result of `MediaQuery.sizeOf`.
|
||||||
|
final GlobalKey? boundaryKey;
|
||||||
|
|
||||||
|
/// The currently selected value or `null` if no item is selected.
|
||||||
|
///
|
||||||
|
/// This value is used to build the button.
|
||||||
|
final SuperEditorDemoIconItem? value;
|
||||||
|
|
||||||
|
/// The items that will be displayed in the popover list.
|
||||||
|
///
|
||||||
|
/// For each item, its [SuperEditorDemoIconItem.icon] is displayed.
|
||||||
|
final List<SuperEditorDemoIconItem> items;
|
||||||
|
|
||||||
|
/// Called when the user selects an item on the popover list.
|
||||||
|
final void Function(SuperEditorDemoIconItem? value) onSelected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SuperEditorDemoIconItemSelector> createState() =>
|
||||||
|
_SuperEditorDemoIconItemSelectorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SuperEditorDemoIconItemSelectorState
|
||||||
|
extends State<SuperEditorDemoIconItemSelector> {
|
||||||
|
/// Shows and hides the popover.
|
||||||
|
final PopoverController _popoverController = PopoverController();
|
||||||
|
|
||||||
|
/// The [FocusNode] of the popover list.
|
||||||
|
final FocusNode _popoverFocusNode = FocusNode();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_popoverController.dispose();
|
||||||
|
_popoverFocusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PopoverScaffold(
|
||||||
|
controller: _popoverController,
|
||||||
|
buttonBuilder: _buildButton,
|
||||||
|
popoverFocusNode: _popoverFocusNode,
|
||||||
|
parentFocusNode: widget.parentFocusNode,
|
||||||
|
popoverBuilder: (context) => RoundedRectanglePopoverAppearance(
|
||||||
|
child: ItemSelectionList<SuperEditorDemoIconItem>(
|
||||||
|
value: widget.value,
|
||||||
|
items: widget.items,
|
||||||
|
itemBuilder: _buildItem,
|
||||||
|
onItemSelected: _onItemSelected,
|
||||||
|
onCancel: () => _popoverController.close(),
|
||||||
|
focusNode: _popoverFocusNode,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildItem(BuildContext context, SuperEditorDemoIconItem item,
|
||||||
|
bool isActive, VoidCallback onTap) {
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isActive ? Colors.grey.withOpacity(0.2) : Colors.transparent,
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
constraints:
|
||||||
|
const BoxConstraints(minHeight: kMinInteractiveDimension),
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
|
child: Icon(item.icon),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildButton(BuildContext context) {
|
||||||
|
return SuperEditorPopoverButton(
|
||||||
|
onTap: () => _popoverController.open(),
|
||||||
|
padding: const EdgeInsets.only(left: 8.0, right: 24),
|
||||||
|
child: widget.value == null //
|
||||||
|
? const SizedBox()
|
||||||
|
: Icon(widget.value!.icon),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onItemSelected(SuperEditorDemoIconItem? value) {
|
||||||
|
_popoverController.close();
|
||||||
|
widget.onSelected(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An option that is displayed as an icon by a [SuperEditorDemoIconItemSelector].
|
||||||
|
///
|
||||||
|
/// Two [SuperEditorDemoIconItem]s are considered to be equal if they have the same [id].
|
||||||
|
class SuperEditorDemoIconItem {
|
||||||
|
const SuperEditorDemoIconItem({
|
||||||
|
required this.id,
|
||||||
|
required this.icon,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The value that identifies this item.
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// The icon that is displayed.
|
||||||
|
final IconData icon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is SuperEditorDemoIconItem &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
id == other.id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => id.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A button with a center-left aligned [child] and a right aligned arrow icon.
|
||||||
|
///
|
||||||
|
/// The arrow is displayed above the [child].
|
||||||
|
class SuperEditorPopoverButton extends StatelessWidget {
|
||||||
|
const SuperEditorPopoverButton({
|
||||||
|
super.key,
|
||||||
|
this.padding,
|
||||||
|
required this.onTap,
|
||||||
|
this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Padding around the [child].
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
|
/// Called when the user taps the button.
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
/// The Widget displayed inside this button.
|
||||||
|
///
|
||||||
|
/// If `null`, only the arrow is displayed.
|
||||||
|
final Widget? child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Center(
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
children: [
|
||||||
|
if (child != null) //
|
||||||
|
Padding(
|
||||||
|
padding: padding ?? EdgeInsets.zero,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
const Positioned(
|
||||||
|
right: 0,
|
||||||
|
child: Icon(Icons.arrow_drop_down),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
//import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
//import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:follow_the_leader/follow_the_leader.dart';
|
import 'package:follow_the_leader/follow_the_leader.dart';
|
||||||
|
import 'package:invoiceninja_flutter/utils/super_editor/super_editor_item_selector.dart';
|
||||||
import 'package:overlord/follow_the_leader.dart';
|
import 'package:overlord/follow_the_leader.dart';
|
||||||
import 'package:super_editor/super_editor.dart';
|
import 'package:super_editor/super_editor.dart';
|
||||||
|
|
||||||
|
|
@ -103,7 +104,6 @@ class _EditorToolbarState extends State<EditorToolbar> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
/// Returns true if the currently selected text node is capable of being
|
/// Returns true if the currently selected text node is capable of being
|
||||||
/// transformed into a different type text node, returns false if
|
/// transformed into a different type text node, returns false if
|
||||||
/// multiple nodes are selected, no node is selected, or the selected
|
/// multiple nodes are selected, no node is selected, or the selected
|
||||||
|
|
@ -147,6 +147,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.
|
||||||
|
|
@ -184,6 +185,7 @@ class _EditorToolbarState extends State<EditorToolbar> {
|
||||||
final selectedNode = widget.document.getNodeById(selection.extent.nodeId);
|
final selectedNode = widget.document.getNodeById(selection.extent.nodeId);
|
||||||
return selectedNode is ParagraphNode;
|
return selectedNode is ParagraphNode;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
/// Converts the currently selected text node into a new type of
|
/// Converts the currently selected text node into a new type of
|
||||||
/// text node, represented by [newType].
|
/// text node, represented by [newType].
|
||||||
|
|
@ -260,7 +262,6 @@ class _EditorToolbarState extends State<EditorToolbar> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
/// Toggles bold styling for the current selected text.
|
/// Toggles bold styling for the current selected text.
|
||||||
void _toggleBold() {
|
void _toggleBold() {
|
||||||
|
|
@ -464,6 +465,7 @@ class _EditorToolbarState extends State<EditorToolbar> {
|
||||||
.getNodeById(widget.composer.selection!.extent.nodeId) as ParagraphNode;
|
.getNodeById(widget.composer.selection!.extent.nodeId) as ParagraphNode;
|
||||||
selectedNode.putMetadataValue('textAlign', newAlignmentValue);
|
selectedNode.putMetadataValue('textAlign', newAlignmentValue);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
/// Returns the localized name for the given [_TextType], e.g.,
|
/// Returns the localized name for the given [_TextType], e.g.,
|
||||||
/// "Paragraph" or "Header 1".
|
/// "Paragraph" or "Header 1".
|
||||||
|
|
@ -492,7 +494,17 @@ class _EditorToolbarState extends State<EditorToolbar> {
|
||||||
return 'Unordered List Item';
|
return 'Unordered List Item';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
/// Called when the user selects a block type on the toolbar.
|
||||||
|
void _onBlockTypeSelected(SuperEditorDemoTextItem? selectedItem) {
|
||||||
|
if (selectedItem != null) {
|
||||||
|
setState(() {
|
||||||
|
_convertTextToNewType(_TextType.values //
|
||||||
|
.where((e) => e.name == selectedItem.id)
|
||||||
|
.first);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _onPerformAction(TextInputAction action) {
|
void _onPerformAction(TextInputAction action) {
|
||||||
if (action == TextInputAction.done) {
|
if (action == TextInputAction.done) {
|
||||||
|
|
@ -547,38 +559,16 @@ class _EditorToolbarState extends State<EditorToolbar> {
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
/*
|
|
||||||
// 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()) ...[
|
||||||
Tooltip(
|
Tooltip(
|
||||||
//message: AppLocalizations.of(context)!.labelTextBlockType,
|
//message: AppLocalizations.of(context)!.labelTextBlockType,
|
||||||
message: 'Block Type',
|
message: 'Block Type',
|
||||||
child: DropdownButton<_TextType>(
|
child: _buildBlockTypeSelector(),
|
||||||
value: _getCurrentTextType(),
|
|
||||||
items: _TextType.values
|
|
||||||
.map((textType) => DropdownMenuItem<_TextType>(
|
|
||||||
value: textType,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16.0),
|
|
||||||
child: Text(_getTextTypeName(textType)),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
icon: const Icon(Icons.arrow_drop_down),
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.black,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
underline: const SizedBox(),
|
|
||||||
elevation: 0,
|
|
||||||
itemHeight: 48,
|
|
||||||
onChanged: _convertTextToNewType,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
_buildVerticalDivider(),
|
_buildVerticalDivider(),
|
||||||
],
|
],
|
||||||
*/
|
|
||||||
Center(
|
Center(
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: _toggleBold,
|
onPressed: _toggleBold,
|
||||||
|
|
@ -673,6 +663,27 @@ class _EditorToolbarState extends State<EditorToolbar> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildBlockTypeSelector() {
|
||||||
|
final currentBlockType = _getCurrentTextType();
|
||||||
|
return SuperEditorDemoTextItemSelector(
|
||||||
|
parentFocusNode: widget.editorFocusNode,
|
||||||
|
boundaryKey: widget.editorViewportKey,
|
||||||
|
id: SuperEditorDemoTextItem(
|
||||||
|
id: currentBlockType.name,
|
||||||
|
label: _getTextTypeName(currentBlockType),
|
||||||
|
),
|
||||||
|
items: _TextType.values
|
||||||
|
.map(
|
||||||
|
(blockType) => SuperEditorDemoTextItem(
|
||||||
|
id: blockType.name,
|
||||||
|
label: _getTextTypeName(blockType),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onSelected: _onBlockTypeSelected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildUrlField() {
|
Widget _buildUrlField() {
|
||||||
return Material(
|
return Material(
|
||||||
shape: const StadiumBorder(),
|
shape: const StadiumBorder(),
|
||||||
|
|
@ -735,7 +746,6 @@ class _EditorToolbarState extends State<EditorToolbar> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Widget _buildVerticalDivider() {
|
Widget _buildVerticalDivider() {
|
||||||
return Container(
|
return Container(
|
||||||
width: 1,
|
width: 1,
|
||||||
|
|
@ -743,6 +753,7 @@ class _EditorToolbarState extends State<EditorToolbar> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
IconData _buildTextAlignIcon(TextAlign align) {
|
IconData _buildTextAlignIcon(TextAlign align) {
|
||||||
switch (align) {
|
switch (align) {
|
||||||
case TextAlign.left:
|
case TextAlign.left:
|
||||||
|
|
@ -760,7 +771,6 @@ class _EditorToolbarState extends State<EditorToolbar> {
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
enum _TextType {
|
enum _TextType {
|
||||||
header1,
|
header1,
|
||||||
header2,
|
header2,
|
||||||
|
|
@ -770,7 +780,6 @@ enum _TextType {
|
||||||
orderedListItem,
|
orderedListItem,
|
||||||
unorderedListItem,
|
unorderedListItem,
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
/// Small toolbar that is intended to display over an image and
|
/// Small toolbar that is intended to display over an image and
|
||||||
/// offer controls to expand or contract the size of the image.
|
/// offer controls to expand or contract the size of the image.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
name: invoiceninja_flutter
|
name: invoiceninja_flutter
|
||||||
description: Client for Invoice Ninja
|
description: Client for Invoice Ninja
|
||||||
version: 5.0.146+146
|
version: 5.0.147+147
|
||||||
homepage: https://invoiceninja.com
|
homepage: https://invoiceninja.com
|
||||||
documentation: https://invoiceninja.github.io
|
documentation: https://invoiceninja.github.io
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ packages:
|
||||||
description:
|
description:
|
||||||
path: attributed_text
|
path: attributed_text
|
||||||
ref: stable
|
ref: stable
|
||||||
resolved-ref: "88af88145526519a1830336c83bb154ceba7235c"
|
resolved-ref: cf157b787d118db0e80e92d5b10883ab58e9e21c
|
||||||
url: "https://github.com/superlistapp/super_editor"
|
url: "https://github.com/superlistapp/super_editor"
|
||||||
source: git
|
source: git
|
||||||
version: "0.2.2"
|
version: "0.2.2"
|
||||||
|
|
@ -1532,7 +1532,7 @@ packages:
|
||||||
description:
|
description:
|
||||||
path: super_editor
|
path: super_editor
|
||||||
ref: stable
|
ref: stable
|
||||||
resolved-ref: "88af88145526519a1830336c83bb154ceba7235c"
|
resolved-ref: cf157b787d118db0e80e92d5b10883ab58e9e21c
|
||||||
url: "https://github.com/superlistapp/super_editor"
|
url: "https://github.com/superlistapp/super_editor"
|
||||||
source: git
|
source: git
|
||||||
version: "0.2.6"
|
version: "0.2.6"
|
||||||
|
|
@ -1541,7 +1541,7 @@ packages:
|
||||||
description:
|
description:
|
||||||
path: super_editor_markdown
|
path: super_editor_markdown
|
||||||
ref: stable
|
ref: stable
|
||||||
resolved-ref: "88af88145526519a1830336c83bb154ceba7235c"
|
resolved-ref: cf157b787d118db0e80e92d5b10883ab58e9e21c
|
||||||
url: "https://github.com/superlistapp/super_editor"
|
url: "https://github.com/superlistapp/super_editor"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.5"
|
version: "0.1.5"
|
||||||
|
|
@ -1550,7 +1550,7 @@ packages:
|
||||||
description:
|
description:
|
||||||
path: super_text_layout
|
path: super_text_layout
|
||||||
ref: stable
|
ref: stable
|
||||||
resolved-ref: "88af88145526519a1830336c83bb154ceba7235c"
|
resolved-ref: cf157b787d118db0e80e92d5b10883ab58e9e21c
|
||||||
url: "https://github.com/superlistapp/super_editor"
|
url: "https://github.com/superlistapp/super_editor"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.8"
|
version: "0.1.8"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
name: invoiceninja_flutter
|
name: invoiceninja_flutter
|
||||||
description: Client for Invoice Ninja
|
description: Client for Invoice Ninja
|
||||||
version: 5.0.146+146
|
version: 5.0.147+147
|
||||||
homepage: https://invoiceninja.com
|
homepage: https://invoiceninja.com
|
||||||
documentation: https://invoiceninja.github.io
|
documentation: https://invoiceninja.github.io
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
name: invoiceninja
|
name: invoiceninja
|
||||||
version: '5.0.146'
|
version: '5.0.147'
|
||||||
summary: Create invoices, accept payments, track expenses & time tasks
|
summary: Create invoices, accept payments, track expenses & time tasks
|
||||||
description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead
|
description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue