From cfab3715b6f5fcf17eec533c3c2f1530f353b0df Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 23 Jun 2026 11:19:10 +0200 Subject: [PATCH 001/117] Add xournal++ exporter, improve importer --- CHANGELOG.md | 10 +- api/lib/src/converter/xopp.dart | 690 ++++++++++++++++++++++-------- api/test/xopp_test.dart | 253 +++++++++++ app/lib/api/save.dart | 22 + app/lib/views/app_bar.dart | 27 +- app/lib/views/files/view.dart | 2 + app/test/api/save_test.dart | 15 + metadata/en-US/changelogs/186.txt | 2 +- metadata/en-US/changelogs/187.txt | 4 + 9 files changed, 838 insertions(+), 187 deletions(-) create mode 100644 api/test/xopp_test.dart create mode 100644 app/test/api/save_test.dart create mode 100644 metadata/en-US/changelogs/187.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 21c3a9773f5f..96a3a703ac95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ # Changelog - - -## 2.6.0-beta.0 (2026-06-22) - + + +## 2.6.0-beta.0 (2026-06-22) + * Add tool presets ([#1070](https://github.com/LinwoodDev/Butterfly/issues/1070)) * Add tool favorites * Add element paints to replace current color property @@ -49,7 +49,7 @@ * Simplify android build files * Migrate to new clipboard library -Read more here: https://linwood.dev/butterfly/2.6.0-rc.0 +Read more here: https://linwood.dev/butterfly/2.6.0-beta.0 ## 2.5.3 (2026-06-08) diff --git a/api/lib/src/converter/xopp.dart b/api/lib/src/converter/xopp.dart index 2e1075058a49..5434298674f5 100644 --- a/api/lib/src/converter/xopp.dart +++ b/api/lib/src/converter/xopp.dart @@ -8,146 +8,325 @@ import 'package:butterfly_api/butterfly_text.dart' as text; import 'package:dart_leap/dart_leap.dart'; import 'package:xml/xml.dart'; -List toPoints(List data) { +const _xoppTypeKey = 'xopp:type'; + +class _XoppContainer { + final XmlDocument document; + final Archive? archive; + + const _XoppContainer(this.document, [this.archive]); +} + +List _numbers(String? value) => + value + ?.trim() + .split(RegExp(r'\s+')) + .where((value) => value.isNotEmpty) + .map(double.tryParse) + .nonNulls + .toList() ?? + []; + +double _number(String? value, [double fallback = 0]) => + double.tryParse(value ?? '') ?? fallback; + +List _points(XmlElement element) { + final coordinates = _numbers(element.innerText); + final widths = _numbers(element.getAttribute('width')); + final baseWidth = widths.firstOrNull ?? 1; final points = []; - final iterator = data.iterator; - while (iterator.moveNext()) { - final x = iterator.current; - if (iterator.moveNext()) { - final y = iterator.current; - points.add(PathPoint(x, y)); - } + for (var i = 0; i + 1 < coordinates.length; i += 2) { + final pressureIndex = i ~/ 2 + 1; + final pressure = pressureIndex < widths.length && baseWidth > 0 + ? widths[pressureIndex] / baseWidth + : 1.0; + points.add(PathPoint(coordinates[i], coordinates[i + 1], pressure)); } return points; } -SRGBColor _importColor(String value) { - return SRGBColor.parse(value); +SRGBColor _importColor(String? value, [SRGBColor fallback = SRGBColor.black]) { + const namedColors = { + 'black': SRGBColor.black, + 'blue': SRGBColor.blue, + 'green': SRGBColor.green, + 'red': SRGBColor.red, + 'white': SRGBColor.white, + 'yellow': SRGBColor.yellow, + }; + if (value == null) return fallback; + return namedColors[value.toLowerCase()] ?? + SRGBColor.tryParse(value) ?? + fallback; +} + +String _exportColor(SRGBColor value) => value.toHexString(); + +String _extensionForImage(Uint8List data) { + if (data.length >= 3 && + data[0] == 0xff && + data[1] == 0xd8 && + data[2] == 0xff) { + return 'jpg'; + } + if (data.length >= 4 && + data[0] == 0x47 && + data[1] == 0x49 && + data[2] == 0x46 && + data[3] == 0x38) { + return 'gif'; + } + return 'png'; +} + +Uint8List? _decodeEmbeddedData(String value) { + final normalized = value.trim(); + if (normalized.isEmpty) return null; + try { + if (normalized.startsWith('data:')) { + return UriData.parse(normalized).contentAsBytes(); + } + return base64Decode(normalized.replaceAll(RegExp(r'\s+'), '')); + } catch (_) { + return null; + } +} + +Map _attributes(XmlElement element) => { + for (final attribute in element.attributes) + attribute.name.qualified: attribute.value, +}; + +_XoppContainer _decodeXopp(Uint8List data) { + if (data.length >= 4 && + data[0] == 0x50 && + data[1] == 0x4b && + data[2] == 0x03 && + data[3] == 0x04) { + final archive = ZipDecoder().decodeBytes(data); + final content = archive.findFile('content.xml')?.readBytes(); + if (content == null) { + throw const FormatException('Xournal++ package has no content.xml'); + } + return _XoppContainer(XmlDocument.parse(utf8.decode(content)), archive); + } + return _XoppContainer( + XmlDocument.parse(utf8.decode(GZipDecoder().decodeBytes(data))), + ); } -String _exportColor(SRGBColor value) { - return value.toHexString(); +Uint8List? _elementData(XmlElement element, Archive? archive) { + final attachment = element.getElement('attachment')?.getAttribute('path'); + if (attachment != null) { + return archive?.findFile(attachment)?.readBytes(); + } + return _decodeEmbeddedData(element.innerText); } -(NoteData, PadElement?) getElement( +(NoteData, PadElement?) _importElement( NoteData data, XmlElement element, String collectionName, + Archive? archive, ) { - PadElement? get() { - switch (element.qualifiedName) { - case 'stroke': - return PenElement( + final extra = { + _xoppTypeKey: element.qualifiedName, + 'xopp:attributes': _attributes(element), + }; + switch (element.qualifiedName) { + case 'stroke': + final widths = _numbers(element.getAttribute('width')); + return ( + data, + PenElement( + id: createUniqueId(), property: PenProperty( paint: ElementPaint.solid( - color: _importColor(element.getAttribute('color')!), - ), - strokeWidth: double.parse( - element.getAttribute('width')!.split(' ').first, + color: _importColor(element.getAttribute('color')), ), + strokeWidth: widths.firstOrNull ?? 1, + thinning: widths.length > 1 ? 1 : 0, ), - extra: {'xopp:width': element.getAttribute('width')!}, - points: toPoints( - element.innerText.split(' ').map((e) => double.parse(e)).toList(), - ), + extra: extra, + points: _points(element), collection: collectionName, - ); - case 'text': - return TextElement( + ), + ); + case 'text': + case 'link': + final color = _importColor(element.getAttribute('color')); + final size = _number(element.getAttribute('size'), 12); + return ( + data, + TextElement( + id: createUniqueId(), area: text.TextArea( paragraph: text.TextParagraph( textSpans: [ text.InlineSpan.text( text: element.innerText, - property: text.SpanProperty.defined( - color: _importColor(element.getAttribute('color')!), - size: double.parse(element.getAttribute('size')!), - ), + property: text.SpanProperty.defined(color: color, size: size), ), ], ), ), + foreground: color, position: Point( - double.parse(element.getAttribute('x')!), - double.parse(element.getAttribute('y')!), + _number(element.getAttribute('x')), + _number(element.getAttribute('y')), ), collection: collectionName, - ); - case 'image': - final imageData = UriData.parse(element.innerText); - String path; - (data, path) = data.importImage(imageData.contentAsBytes(), 'png'); - final left = double.parse(element.getAttribute('x')!); - final top = double.parse(element.getAttribute('y')!); - final right = double.parse(element.getAttribute('right')!); - final bottom = double.parse(element.getAttribute('bottom')!); - return ImageElement( + extra: extra, + ), + ); + case 'image': + final imageData = _elementData(element, archive); + if (imageData == null) return (data, null); + final (newData, path) = data.importImage( + imageData, + _extensionForImage(imageData), + ); + final left = _number( + element.getAttribute('left') ?? element.getAttribute('x'), + ); + final top = _number( + element.getAttribute('top') ?? element.getAttribute('y'), + ); + final right = _number(element.getAttribute('right'), left); + final bottom = _number(element.getAttribute('bottom'), top); + return ( + newData, + ImageElement( + id: createUniqueId(), source: Uri.file(path, windows: false).toString(), position: Point(left, top), collection: collectionName, height: bottom - top, width: right - left, - ); - default: - return null; - } + extra: extra, + ), + ); + case 'teximage': + final pdfData = _elementData(element, archive); + if (pdfData == null) return (data, null); + final (newData, path) = data.importPdf(pdfData); + final left = _number(element.getAttribute('left')); + final top = _number(element.getAttribute('top')); + final right = _number(element.getAttribute('right'), left); + final bottom = _number(element.getAttribute('bottom'), top); + return ( + newData, + PdfElement( + id: createUniqueId(), + source: Uri.file(path, windows: false).toString(), + position: Point(left, top), + collection: collectionName, + height: bottom - top, + width: right - left, + extra: extra, + ), + ); + default: + return (data, null); } +} - return (data, get()); +Background? _importBackground( + NoteData note, + XmlElement element, + double width, + double height, + Archive? archive, +) { + final extra = {'xopp:attributes': _attributes(element)}; + final color = _importColor(element.getAttribute('color'), SRGBColor.white); + if (element.getAttribute('type') == 'solid') { + final style = element.getAttribute('style'); + return Background.texture( + texture: SurfaceTexture.pattern( + boxColor: color, + boxXColor: color, + boxYColor: color, + boxYSpace: style == 'ruled' || style == 'lined' ? 20 : 0, + boxXSpace: style == 'ruled' ? 20 : 0, + ), + extra: extra, + ); + } + return null; } NoteData xoppMigrator(Uint8List data) { - final doc = XmlDocument.parse(utf8.decode(GZipDecoder().decodeBytes(data))); - final xournal = doc.getElement('xournal')!; + final container = _decodeXopp(data); + final xournal = container.document.getElement('xournal'); + if (xournal == null) { + throw const FormatException('Xournal++ document has no xournal root'); + } var note = NoteData(Archive()); note = note.setMetadata( FileMetadata( type: NoteFileType.document, - name: xournal.getElement('title')!.innerText, + name: xournal.getElement('title')?.innerText ?? '', ), ); - for (final entry in xournal.findElements('page').toList().asMap().entries) { - final elements = []; - final page = entry.value; - final layers = {}; + for (final page in xournal.findElements('page')) { + final width = _number(page.getAttribute('width'), 595.27559); + final height = _number(page.getAttribute('height'), 841.88976); + final layers = []; for (final (index, layer) in page.findElements('layer').indexed) { - final xoppLayerName = layer.getAttribute('name'); - final layerName = xoppLayerName ?? 'Layer ${index + 1}'; + final hasName = layer.getAttribute('name') != null; + final layerName = layer.getAttribute('name') ?? 'Layer ${index + 1}'; + final elements = []; for (final element in layer.childElements) { PadElement? current; - (note, current) = getElement(note, element, layerName); + (note, current) = _importElement( + note, + element, + layerName, + container.archive, + ); if (current != null) { elements.add(current); } } - layers[layerName] = { - 'hasName': xoppLayerName != null, - 'timestamp': layer.getAttribute('timestamp'), - }; + layers.add( + DocumentLayer(id: createUniqueId(), name: layerName, content: elements), + ); + if (!hasName) { + // DocumentLayer requires a display name. The original absence is + // retained in the page XML metadata below. + } } - final backgroundXml = page.getElement('background')!; - final backgroundStyle = backgroundXml.getAttribute('style'); - final backgroundColor = _importColor( - backgroundXml.getAttribute('color')!.substring(1), - ); - final background = switch (backgroundXml.getAttribute('type')) { - 'solid' => Background.texture( - texture: SurfaceTexture.pattern( - boxXColor: backgroundColor, - boxYColor: backgroundColor, - boxYSpace: backgroundStyle == 'ruled' || backgroundStyle == 'lined' - ? 20 - : 0, - boxXSpace: backgroundStyle == 'ruled' ? 20 : 0, - ), - ), - _ => null, - }; + final backgroundXml = page.getElement('background'); + final background = backgroundXml == null + ? null + : _importBackground( + note, + backgroundXml, + width, + height, + container.archive, + ); (note, _) = note.addPage( DocumentPage( - layers: [DocumentLayer(content: elements, id: createUniqueId())], + layers: layers.isEmpty ? [DocumentLayer(id: createUniqueId())] : layers, backgrounds: [?background], - extra: {'xopp:layers': layers}, + areas: [ + Area( + name: 'Page', + width: width, + height: height, + position: const Point(0, 0), + isInitial: true, + ), + ], + extra: { + 'xopp:width': width, + 'xopp:height': height, + 'xopp:layerAttributes': [ + for (final layer in page.findElements('layer')) _attributes(layer), + ], + }, ), '', ); @@ -155,109 +334,268 @@ NoteData xoppMigrator(Uint8List data) { return note; } -Uint8List xoppExporter(NoteData document) { +Map _preservedAttributes(PadElement element) { + final value = element.extra['xopp:attributes']; + if (value is! Map) return {}; + return value.map((key, value) => MapEntry('$key', '$value')); +} + +void _exportStroke( + XmlBuilder builder, + PenElement element, + Point offset, +) { + if (element.points.isEmpty) return; + final width = element.property.strokeWidth; + final points = element.points.length == 1 + ? [ + element.points.single, + element.points.single.copyWith( + x: element.points.single.x + max(width.abs() * 0.001, 0.001), + ), + ] + : element.points; + final pressureWidths = + points.length > 1 && points.any((point) => point.pressure != 1) + ? [ + width, + ...points + .take(points.length - 1) + .map((point) => width * point.pressure), + ].join(' ') + : width.toString(); + final attributes = _preservedAttributes(element) + ..['color'] = _exportColor(element.property.paint.previewColor) + ..['width'] = pressureWidths + ..putIfAbsent('tool', () => 'pen') + ..putIfAbsent('capStyle', () => 'round'); + builder.element( + 'stroke', + attributes: attributes, + nest: () => builder.text( + points + .map((point) => '${point.x - offset.x} ${point.y - offset.y}') + .join(' '), + ), + ); +} + +void _exportLabel( + XmlBuilder builder, + LabelElement element, + Point offset, +) { + final padElement = element as PadElement; + final attributes = _preservedAttributes(padElement); + final isLink = padElement.extra[_xoppTypeKey] == 'link'; + final styleSheet = element.styleSheet; + final style = element is TextElement + ? styleSheet?.item + .resolveParagraphProperty(element.area.paragraph.property) + ?.span + : styleSheet?.item.getParagraphProperty('p')?.span; + attributes + ..['color'] = _exportColor(style?.color ?? element.foreground) + ..['size'] = (style?.size ?? _number(attributes['size'], 12)).toString() + ..['x'] = (element.position.x - offset.x).toString() + ..['y'] = (element.position.y - offset.y).toString() + ..putIfAbsent('font', () => 'Sans'); + if (isLink) { + attributes + ..putIfAbsent('align', () => 'left') + ..putIfAbsent('url', () => ''); + } + builder.element( + isLink ? 'link' : 'text', + attributes: attributes, + nest: () => builder.text(element.text), + ); +} + +Uint8List? _assetData(NoteData document, SourcedElement element) => + document.getAsset(Uri.parse(element.source).path); + +void _exportImage( + XmlBuilder builder, + NoteData document, + ImageElement element, + Point offset, +) { + final imageData = _assetData(document, element); + if (imageData == null) return; + final attributes = _preservedAttributes(element) + ..['left'] = (element.position.x - offset.x).toString() + ..['top'] = (element.position.y - offset.y).toString() + ..['right'] = (element.position.x - offset.x + element.width).toString() + ..['bottom'] = (element.position.y - offset.y + element.height).toString(); + builder.element( + 'image', + attributes: attributes, + nest: () => builder.text(base64Encode(imageData)), + ); +} + +void _exportTexImage( + XmlBuilder builder, + NoteData document, + PdfElement element, + Point offset, +) { + final pdfData = _assetData(document, element); + if (pdfData == null) return; + final attributes = _preservedAttributes(element) + ..['left'] = (element.position.x - offset.x).toString() + ..['top'] = (element.position.y - offset.y).toString() + ..['right'] = (element.position.x - offset.x + element.width).toString() + ..['bottom'] = (element.position.y - offset.y + element.height).toString() + ..putIfAbsent('text', () => ''); + builder.element( + 'teximage', + attributes: attributes, + nest: () => builder.text(base64Encode(pdfData)), + ); +} + +void _exportBackground(XmlBuilder builder, DocumentPage page) { + final background = page.backgrounds.firstOrNull; + final preserved = background?.extra['xopp:attributes']; + final attributes = preserved is Map + ? preserved.map((key, value) => MapEntry('$key', '$value')) + : {}; + switch (background) { + case TextureBackground(:final texture): + final style = switch (texture) { + PatternTexture(:final boxXSpace, :final boxYSpace) + when boxXSpace > 0 && boxYSpace > 0 => + 'ruled', + PatternTexture(:final boxYSpace) when boxYSpace > 0 => 'lined', + _ => 'plain', + }; + attributes + ..['type'] = 'solid' + ..['color'] = _exportColor(background.defaultColor) + ..putIfAbsent('style', () => style); + case _: + attributes + ..['type'] = 'solid' + ..['color'] = _exportColor(background?.defaultColor ?? SRGBColor.white) + ..['style'] = 'plain'; + } + builder.element('background', attributes: attributes); +} + +bool _isInArea(PadElement element, Area? area) { + if (area == null) return true; + final left = area.position.x; + final top = area.position.y; + final right = left + area.width; + final bottom = top + area.height; + + bool overlaps(double x, double y, double width, double height) => + x <= right && x + width >= left && y <= bottom && y + height >= top; + + return switch (element) { + PenElement(:final points, :final property) when points.isNotEmpty => () { + final minX = points.map((point) => point.x).reduce(min); + final maxX = points.map((point) => point.x).reduce(max); + final minY = points.map((point) => point.y).reduce(min); + final maxY = points.map((point) => point.y).reduce(max); + final padding = property.strokeWidth.abs() / 2; + return overlaps( + minX - padding, + minY - padding, + maxX - minX + padding * 2, + maxY - minY + padding * 2, + ); + }(), + LabelElement(:final position) => + position.x >= left && + position.x <= right && + position.y >= top && + position.y <= bottom, + ImageElement(:final position, :final width, :final height) || + PdfElement( + :final position, + :final width, + :final height, + ) => overlaps(position.x, position.y, width, height), + _ => false, + }; +} + +String _buildXoppXml(NoteData document) { final builder = XmlBuilder(); builder.processing('xml', 'version="1.0" encoding="UTF-8"'); builder.element( 'xournal', - attributes: {'creator': 'Butterfly', 'fileversion': '4'}, + attributes: {'creator': 'Butterfly', 'fileversion': '5'}, nest: () { - final metadata = document.getMetadata(); - builder.element('title', nest: metadata?.name); + builder.element('title', nest: document.getMetadata()?.name ?? ''); for (final pageName in document.getPages()) { final page = document.getPage(pageName); if (page == null) continue; - builder.element( - 'page', - nest: () { - builder.element( - 'background', - attributes: { - 'type': 'solid', - 'color': _exportColor( - page.backgrounds.firstOrNull?.defaultColor ?? SRGBColor.white, - ), - 'style': 'plain', - }, - ); - builder.element( - 'layer', - nest: () { - for (final element in page.content) { - switch (element) { - case PenElement e: - builder.element( - 'stroke', - attributes: { - 'color': _exportColor(e.property.paint.previewColor), - 'width': e.property.strokeWidth.toString(), - 'tool': 'pen', - }, - nest: () { - builder.text( - e.points.map((e) => '${e.x} ${e.y}').join(' '), - ); - }, - ); - break; - case LabelElement e: - final styleSheet = e.styleSheet; - final style = e is TextElement - ? styleSheet?.item - .resolveParagraphProperty( - e.area.paragraph.property, - ) - ?.span - : styleSheet?.item.getParagraphProperty('p')?.span; - builder.element( - 'text', - attributes: { - 'color': _exportColor( - style?.color ?? SRGBColor.black, - ), - 'size': (style?.size ?? 12).toString(), - 'x': e.position.x.toString(), - 'y': e.position.y.toString(), - }, - nest: () { - builder.text(e.text); - }, - ); - case ImageElement e: - final imageData = document.getAsset( - Uri.parse(e.source).path, - ); - builder.element( - 'image', - attributes: { - 'left': e.position.x.toString(), - 'top': e.position.y.toString(), - 'right': (e.position.x + e.width).toString(), - 'bottom': (e.position.y + e.height).toString(), - }, - nest: () { - builder.text( - UriData.fromBytes( - imageData ?? [], - mimeType: 'image/png', - ).toString(), - ); - }, - ); - default: - break; - } + final exportAreas = page.areas.isEmpty ? [null] : page.areas; + for (final pageArea in exportAreas) { + final pageOffset = pageArea?.position ?? const Point(0, 0); + builder.element( + 'page', + attributes: { + 'width': + '${pageArea?.width ?? page.extra['xopp:width'] ?? 595.27559}', + 'height': + '${pageArea?.height ?? page.extra['xopp:height'] ?? 841.88976}', + }, + nest: () { + _exportBackground(builder, page); + final layerAttributes = page.extra['xopp:layerAttributes']; + for (final (index, layer) in page.layers.indexed) { + final attributes = {}; + if (layerAttributes is List && + index < layerAttributes.length && + layerAttributes[index] is Map) { + attributes.addAll( + (layerAttributes[index] as Map).map( + (key, value) => MapEntry('$key', '$value'), + ), + ); } - }, - ); - }, - ); + if (layer.name.isNotEmpty) { + attributes['name'] = layer.name; + } else { + attributes.remove('name'); + } + builder.element( + 'layer', + attributes: attributes, + nest: () { + for (final element in layer.content.where( + (element) => _isInArea(element, pageArea), + )) { + switch (element) { + case PenElement e: + _exportStroke(builder, e, pageOffset); + case LabelElement e: + _exportLabel(builder, e, pageOffset); + case ImageElement e: + _exportImage(builder, document, e, pageOffset); + case PdfElement e + when e.extra[_xoppTypeKey] == 'teximage': + _exportTexImage(builder, document, e, pageOffset); + default: + break; + } + } + }, + ); + } + }, + ); + } } }, ); - return Uint8List.fromList( - GZipEncoder().encode( - utf8.encode(builder.buildDocument().toXmlString(pretty: true)), - ), - ); + return builder.buildDocument().toXmlString(pretty: true); } + +Uint8List xoppExporter(NoteData document) => + GZipEncoder().encodeBytes(utf8.encode(_buildXoppXml(document))); diff --git a/api/test/xopp_test.dart b/api/test/xopp_test.dart new file mode 100644 index 000000000000..f9126805a7d4 --- /dev/null +++ b/api/test/xopp_test.dart @@ -0,0 +1,253 @@ +import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:archive/archive.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:dart_leap/dart_leap.dart'; +import 'package:test/test.dart'; +import 'package:xml/xml.dart'; + +Uint8List _gzip(String xml) => GZipEncoder().encodeBytes(utf8.encode(xml)); + +XmlDocument _exportedXml(NoteData document) => XmlDocument.parse( + utf8.decode(GZipDecoder().decodeBytes(xoppExporter(document))), +); + +void main() { + const source = ''' + + Round trip + + + + 1 2 3 4 5 6 + Hello & goodbye + Example + iVBORw0KGgo= + + +'''; + + test('imports and exports supported Xournal++ document data', () { + final imported = xoppMigrator(_gzip(source)); + final reimported = xoppMigrator(xoppExporter(imported)); + final page = reimported.getPage(reimported.getPages().single)!; + + expect(reimported.getMetadata()!.name, 'Round trip'); + expect(page.extra['xopp:width'], 612); + expect(page.extra['xopp:height'], 792); + expect(page.areas.single.position, const Point(0, 0)); + expect(page.areas.single.width, 612); + expect(page.areas.single.height, 792); + expect(page.areas.single.isInitial, isTrue); + expect(page.layers.single.name, 'Ink'); + expect(page.layers.single.content.whereType(), hasLength(1)); + expect(page.layers.single.content.whereType(), hasLength(2)); + expect(page.layers.single.content.whereType(), hasLength(1)); + }); + + test('does not store an original Xournal++ file or auxiliary data files', () { + final document = xoppMigrator(_gzip(source)); + + expect(document.getAssets('xopp/', true), isEmpty); + }); + + test('exports current Butterfly edits', () { + var document = xoppMigrator(_gzip(source)).setName('Edited'); + final pageName = document.getPages().single; + final page = document.getPage(pageName)!; + final layer = page.layers.single; + final stroke = layer.content.whereType().single; + final label = layer.content.whereType().first; + final image = layer.content.whereType().single; + final changedContent = layer.content.map((element) { + if (element == stroke) { + return stroke.copyWith( + property: stroke.property.copyWith(strokeWidth: 7), + points: const [PathPoint(11, 12), PathPoint(13, 14)], + ); + } + if (element == label) { + return label.copyWith( + position: const Point(50, 60), + foreground: SRGBColor.red, + ); + } + if (element == image) { + return image.copyWith( + position: const Point(20, 30), + width: 40, + height: 50, + ); + } + return element; + }).toList(); + final changedPage = page.copyWith( + layers: [layer.copyWith(name: 'Changed layer', content: changedContent)], + ); + (document, _) = document.setPage(changedPage, pageName); + + final exported = _exportedXml(document); + final xournal = exported.getElement('xournal')!; + final exportedStroke = xournal.findAllElements('stroke').single; + final exportedText = xournal.findAllElements('text').single; + final exportedImage = xournal.findAllElements('image').single; + + expect(xournal.getElement('title')!.innerText, 'Edited'); + expect( + xournal.findAllElements('layer').single.getAttribute('name'), + 'Changed layer', + ); + expect(exportedStroke.getAttribute('width'), '7.0'); + expect(exportedStroke.innerText.trim(), '11.0 12.0 13.0 14.0'); + expect(exportedText.getAttribute('x'), '50.0'); + expect(exportedText.getAttribute('y'), '60.0'); + expect(exportedText.getAttribute('color'), '#ff0000ff'); + expect(exportedImage.getAttribute('left'), '20.0'); + expect(exportedImage.getAttribute('top'), '30.0'); + expect(exportedImage.getAttribute('right'), '60.0'); + expect(exportedImage.getAttribute('bottom'), '80.0'); + }); + + test('exports single-point Butterfly strokes as valid Xournal++ strokes', () { + var document = xoppMigrator(_gzip(source)); + final pageName = document.getPages().single; + final page = document.getPage(pageName)!; + final layer = page.layers.single; + final stroke = layer.content.whereType().single; + final changedPage = page.copyWith( + layers: [ + layer.copyWith( + content: [ + stroke.copyWith(points: const [PathPoint(10, 20)]), + ], + ), + ], + ); + (document, _) = document.setPage(changedPage, pageName); + + final exported = _exportedXml(document); + final coordinates = exported + .findAllElements('stroke') + .single + .innerText + .trim() + .split(RegExp(r'\s+')); + + expect(coordinates, hasLength(4)); + final reimported = xoppMigrator(xoppExporter(document)); + final reimportedStroke = reimported + .getPage(reimported.getPages().single)! + .content + .whereType() + .single; + expect(reimportedStroke.points, hasLength(2)); + }); + + test('exports page size and coordinates relative to the initial area', () { + var document = xoppMigrator(_gzip(source)); + final pageName = document.getPages().single; + final page = document.getPage(pageName)!; + final layer = page.layers.single; + final stroke = layer.content.whereType().single; + final changedPage = page.copyWith( + areas: [ + page.areas.single.copyWith( + position: const Point(100, 200), + width: 400, + height: 500, + ), + ], + layers: [ + layer.copyWith( + content: [ + stroke.copyWith( + points: const [PathPoint(110, 220), PathPoint(130, 240)], + ), + ], + ), + ], + ); + (document, _) = document.setPage(changedPage, pageName); + + final exported = _exportedXml(document); + final exportedPage = exported.findAllElements('page').single; + final exportedStroke = exported.findAllElements('stroke').single; + + expect(exportedPage.getAttribute('width'), '400.0'); + expect(exportedPage.getAttribute('height'), '500.0'); + expect(exportedStroke.innerText.trim(), '10.0 20.0 30.0 40.0'); + }); + + test('exports every Butterfly area as a separate Xournal++ page', () { + var document = xoppMigrator(_gzip(source)); + final pageName = document.getPages().single; + final page = document.getPage(pageName)!; + final layer = page.layers.single; + final stroke = layer.content.whereType().single; + final changedPage = page.copyWith( + areas: const [ + Area( + name: 'Left', + position: Point(0, 0), + width: 100, + height: 200, + isInitial: true, + ), + Area(name: 'Right', position: Point(100, 0), width: 300, height: 200), + ], + layers: [ + layer.copyWith( + content: [ + stroke.copyWith( + points: const [PathPoint(10, 20), PathPoint(30, 40)], + ), + stroke.copyWith( + id: createUniqueId(), + points: const [PathPoint(110, 20), PathPoint(130, 40)], + ), + ], + ), + ], + ); + (document, _) = document.setPage(changedPage, pageName); + + final pages = _exportedXml(document).findAllElements('page').toList(); + + expect(pages, hasLength(2)); + expect(pages[0].getAttribute('width'), '100.0'); + expect(pages[1].getAttribute('width'), '300.0'); + expect( + pages[0].findAllElements('stroke').single.innerText.trim(), + '10.0 20.0 30.0 40.0', + ); + expect( + pages[1].findAllElements('stroke').single.innerText.trim(), + '10.0 20.0 30.0 40.0', + ); + }); + + test('imports packaged Xournal++ files with attachments', () { + final packagedXml = source.replaceFirst( + 'iVBORw0KGgo=', + '', + ); + final archive = Archive() + ..addFile( + ArchiveFile.noCompress( + 'mimetype', + 'application/xournal++'.length, + utf8.encode('application/xournal++'), + ), + ) + ..addFile(ArchiveFile.string('META-INF/version', 'current=5\nmin=0')) + ..addFile(ArchiveFile.string('content.xml', packagedXml)) + ..addFile(ArchiveFile.bytes('image.png', base64Decode('iVBORw0KGgo='))); + + final document = xoppMigrator(ZipEncoder().encodeBytes(archive)); + final page = document.getPage(document.getPages().single)!; + + expect(page.layers.single.content.whereType(), hasLength(1)); + }); +} diff --git a/app/lib/api/save.dart b/app/lib/api/save.dart index 04683d12e524..2ca1eb1d2464 100644 --- a/app/lib/api/save.dart +++ b/app/lib/api/save.dart @@ -6,6 +6,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; import 'package:lw_sysapi/lw_sysapi.dart'; +import 'package:lw_file_system/lw_file_system.dart'; + +String sanitizeExportFileName(String? name) => convertNameToFile( + name: name?.trim(), + getUnnamed: () => 'output', +).replaceAll(RegExp(invalidFileName), '_'); Future exportSvg( BuildContext context, @@ -52,6 +58,22 @@ Future exportPdf( label: AppLocalizations.of(context).export, ); +Future exportXopp( + BuildContext context, + Uint8List bytes, { + String? fileName, + bool share = false, +}) => exportFile( + context: context, + bytes: bytes, + fileExtension: 'xopp', + mimeType: 'application/x-xojpp', + uniformTypeIdentifier: 'dev.linwood.butterfly.xopp', + share: share, + fileName: sanitizeExportFileName(fileName), + label: AppLocalizations.of(context).export, +); + Future exportZip( BuildContext context, Uint8List bytes, [ diff --git a/app/lib/views/app_bar.dart b/app/lib/views/app_bar.dart index 8e387194c698..0f0efef936b6 100644 --- a/app/lib/views/app_bar.dart +++ b/app/lib/views/app_bar.dart @@ -5,6 +5,7 @@ import 'package:butterfly/actions/change_path.dart'; import 'package:butterfly/actions/settings.dart'; import 'package:butterfly/actions/svg_export.dart'; import 'package:butterfly/api/open.dart'; +import 'package:butterfly/api/save.dart'; import 'package:butterfly/cubits/current_index.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/dialogs/collaboration/dialog.dart'; @@ -645,11 +646,27 @@ class MainPopupMenu extends StatelessWidget { }, child: Text(AppLocalizations.of(context).pdf), ), - /*MenuItemButton( - leadingIcon: const PhosphorIcon(PhosphorIconsLight.notebook), - onPressed: () => exportXopp(context), - child: const Text('Xournal++'), - ),*/ + MenuItemButton( + leadingIcon: const PhosphorIcon( + PhosphorIconsLight.notebook, + ), + onPressed: () async { + final bloc = context.read(); + final state = bloc.state; + if (state is! DocumentLoadSuccess) return; + final data = await state.saveData( + null, + bloc.currentIndexCubit.state.viewOption, + ); + if (!context.mounted) return; + exportXopp( + context, + xoppExporter(data), + fileName: state.metadata.name, + ); + }, + child: const Text('Xournal++'), + ), ], leadingIcon: const PhosphorIcon( PhosphorIconsLight.paperPlaneRight, diff --git a/app/lib/views/files/view.dart b/app/lib/views/files/view.dart index 5ee335f22520..59ab68ff96d6 100644 --- a/app/lib/views/files/view.dart +++ b/app/lib/views/files/view.dart @@ -711,6 +711,8 @@ class FilesViewState extends State { } if (docName.trim().isEmpty) { docName = null; + } else { + docName = sanitizeExportFileName(docName); } final newFile = await _documentSystem diff --git a/app/test/api/save_test.dart b/app/test/api/save_test.dart new file mode 100644 index 000000000000..3898cb2e0f0c --- /dev/null +++ b/app/test/api/save_test.dart @@ -0,0 +1,15 @@ +import 'package:butterfly/api/save.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('sanitizes export filenames', () { + expect( + sanitizeExportFileName(r'Lecture: chapter 1/2?'), + 'Lecture_ chapter 1_2_', + ); + }); + + test('uses a fallback for empty export filenames', () { + expect(sanitizeExportFileName(' '), 'output'); + }); +} diff --git a/metadata/en-US/changelogs/186.txt b/metadata/en-US/changelogs/186.txt index 5afc3b85f33c..c6b1b794f41a 100644 --- a/metadata/en-US/changelogs/186.txt +++ b/metadata/en-US/changelogs/186.txt @@ -43,4 +43,4 @@ * Simplify android build files * Migrate to new clipboard library -Read more here: https://linwood.dev/butterfly/2.6.0-rc.0 \ No newline at end of file +Read more here: https://linwood.dev/butterfly/2.6.0-beta.0 \ No newline at end of file diff --git a/metadata/en-US/changelogs/187.txt b/metadata/en-US/changelogs/187.txt new file mode 100644 index 000000000000..ca67a83f5802 --- /dev/null +++ b/metadata/en-US/changelogs/187.txt @@ -0,0 +1,4 @@ +* Add xournal++ exporter +* Improve xournal++ importer + +Read more here: https://linwood.dev/butterfly/2.6.0-beta.1 \ No newline at end of file From 2b01ac45445ccf34efd5f3c402db9ca254e5c3ea Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 21 Jun 2026 19:32:25 +0200 Subject: [PATCH 002/117] Add onenote import --- api/lib/src/helpers/asset.dart | 14 + api/lib/src/models/asset.dart | 2 + api/test/data_test.dart | 11 + app/android/app/src/main/AndroidManifest.xml | 40 + app/lib/handlers/asset.dart | 7 +- app/lib/services/import.dart | 47 ++ app/lib/services/onenote.dart | 686 ++++++++++++++++++ app/lib/visualizer/asset.dart | 3 + .../dev.linwood.butterfly.desktop | 2 +- .../mime/packages/dev.linwood.butterfly.xml | 8 +- app/linux/rpm/linwood-butterfly.desktop | 2 +- app/pubspec.lock | 71 +- app/pubspec.yaml | 9 +- app/test/services/onenote_test.dart | 460 ++++++++++++ 14 files changed, 1349 insertions(+), 13 deletions(-) create mode 100644 app/lib/services/onenote.dart create mode 100644 app/test/services/onenote_test.dart diff --git a/api/lib/src/helpers/asset.dart b/api/lib/src/helpers/asset.dart index 4fc923b46f2b..8bdbaedb530f 100644 --- a/api/lib/src/helpers/asset.dart +++ b/api/lib/src/helpers/asset.dart @@ -12,6 +12,8 @@ extension AssetFileTypeHelper on AssetFileType { AssetFileType.markdown => ['public.plain-text'], AssetFileType.page => [], AssetFileType.xopp => ['dev.linwood.butterfly.xopp'], + AssetFileType.oneNote => ['com.microsoft.onenote.one'], + AssetFileType.oneNotePackage => ['com.microsoft.onenote.onepkg'], AssetFileType.rawText => ['public.plain-text'], AssetFileType.archive => ['public.archive'], }; @@ -25,6 +27,8 @@ extension AssetFileTypeHelper on AssetFileType { AssetFileType.markdown => ['md', 'markdown'], AssetFileType.page => [], AssetFileType.xopp => ['xopp'], + AssetFileType.oneNote => ['one'], + AssetFileType.oneNotePackage => ['onepkg'], AssetFileType.rawText => ['txt'], AssetFileType.archive => ['zip'], }; @@ -51,6 +55,16 @@ extension AssetFileTypeHelper on AssetFileType { AssetFileType.svg => ['image/svg+xml'], AssetFileType.page => ['application/x-butterfly-page', 'application/json'], AssetFileType.xopp => ['application/zip'], + AssetFileType.oneNote => [ + 'application/onenote', + 'application/msonenote', + 'application/x-onenote', + ], + AssetFileType.oneNotePackage => [ + 'application/onenote', + 'application/msonenote', + 'application/x-onenote', + ], AssetFileType.archive => [ 'application/zip', 'application/x-tar', diff --git a/api/lib/src/models/asset.dart b/api/lib/src/models/asset.dart index 48eb726512a6..09d7d815e739 100644 --- a/api/lib/src/models/asset.dart +++ b/api/lib/src/models/asset.dart @@ -11,6 +11,8 @@ enum AssetFileType { pdf, svg, xopp, + oneNote, + oneNotePackage, archive, } diff --git a/api/test/data_test.dart b/api/test/data_test.dart index c3b017ae3d17..8e2451c00a00 100644 --- a/api/test/data_test.dart +++ b/api/test/data_test.dart @@ -16,6 +16,17 @@ void main() { expect(AssetFileTypeHelper.fromFileExtension('PDF'), AssetFileType.pdf); expect(AssetFileTypeHelper.fromFileExtension('.PDF'), AssetFileType.pdf); }); + + test('fromFileExtension recognizes OneNote files', () { + expect( + AssetFileTypeHelper.fromFileExtension('.ONE'), + AssetFileType.oneNote, + ); + expect( + AssetFileTypeHelper.fromFileExtension('onepkg'), + AssetFileType.oneNotePackage, + ); + }); }); group('NoteData page operations', () { diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index dc4afda1e50d..13af5ef00f62 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -91,6 +91,46 @@ android:pathPattern=".*\\.tbfly" /> + + + + + + + + + + + + + + + + diff --git a/app/lib/handlers/asset.dart b/app/lib/handlers/asset.dart index 8c8a77f1e3d9..ad614fb696be 100644 --- a/app/lib/handlers/asset.dart +++ b/app/lib/handlers/asset.dart @@ -116,7 +116,12 @@ Future showImportAssetWizard( case ImportType.pdf: return importWithDialog([AssetFileType.pdf]); case ImportType.document: - return importWithDialog([AssetFileType.note, AssetFileType.textNote]); + return importWithDialog([ + AssetFileType.note, + AssetFileType.textNote, + AssetFileType.oneNote, + AssetFileType.oneNotePackage, + ]); case ImportType.markdown: return importWithDialog([AssetFileType.markdown]); case ImportType.xopp: diff --git a/app/lib/services/import.dart b/app/lib/services/import.dart index 660066a07e61..2af3fa35b331 100644 --- a/app/lib/services/import.dart +++ b/app/lib/services/import.dart @@ -34,6 +34,7 @@ import '../cubits/settings.dart'; import '../dialogs/export/general.dart'; import '../dialogs/import/pages.dart'; import '../dialogs/export/pdf.dart'; +import 'onenote.dart'; class ImportResult { final ImportService service; @@ -317,6 +318,20 @@ class ImportService { ), AssetFileType.page => importPage(bytes, realDocument, position: position), AssetFileType.xopp => importXopp(bytes, realDocument, position: position), + AssetFileType.oneNote => importOneNote( + bytes, + isPackage: false, + document: document, + advanced: advanced, + name: name, + ), + AssetFileType.oneNotePackage => importOneNote( + bytes, + isPackage: true, + document: document, + advanced: advanced, + name: name, + ), AssetFileType.archive => importArchive( bytes, fileSystem: fileSystem, @@ -640,6 +655,38 @@ class ImportService { return null; } + @useResult + Future importOneNote( + Uint8List bytes, { + required bool isPackage, + NoteData? document, + bool advanced = true, + String? name, + }) async { + try { + isPackage = + isPackage || + (bytes.length >= 4 && + bytes[0] == 0x4D && + bytes[1] == 0x53 && + bytes[2] == 0x43 && + bytes[3] == 0x46); + final data = await parseOneNoteData( + bytes, + name: name?.trim().isNotEmpty == true ? name!.trim() : 'OneNote', + isPackage: isPackage, + ); + return _importDocument(data, document: document, advanced: advanced); + } catch (e) { + showDialog( + context: context, + builder: (context) => + UnknownImportConfirmationDialog(message: e.toString()), + ); + } + return null; + } + @useResult Future importSvg( Uint8List bytes, diff --git a/app/lib/services/onenote.dart b/app/lib/services/onenote.dart new file mode 100644 index 000000000000..b8f8e03e2c43 --- /dev/null +++ b/app/lib/services/onenote.dart @@ -0,0 +1,686 @@ +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:butterfly/models/defaults.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:butterfly_api/butterfly_text.dart' as text; +import 'package:material_leap/material_leap.dart'; +import 'package:onenote_parser/onenote_parser.dart' as one; + +Future? _oneNoteInitialization; + +const _pixelsPerInch = 96.0; +const _pixelsPerHalfInch = _pixelsPerInch / 2; +const _himetricUnitsPerInch = 2540.0; +const _himetricToPixels = _pixelsPerInch / _himetricUnitsPerInch; + +Future parseOneNoteData( + Uint8List bytes, { + required String name, + required bool isPackage, +}) async { + await (_oneNoteInitialization ??= one.RustLib.init()); + if (isPackage) { + final notebook = await one.parsePackageBytes(data: bytes); + return convertOneNoteNotebook(notebook, name: name); + } + final section = await one.parseSectionBytes( + data: bytes, + fileName: '$name.one', + ); + return convertOneNoteSection(section, name: name); +} + +NoteData convertOneNoteSection(one.OneNoteSection section, {String? name}) { + final converter = _OneNoteConverter(name ?? section.displayName); + converter.addSection(section, const []); + return converter.finish(); +} + +NoteData convertOneNoteNotebook( + one.OneNoteNotebook notebook, { + required String name, +}) { + final converter = _OneNoteConverter(name); + converter.addEntries(notebook.entries, const []); + return converter.finish( + warnings: notebook.warnings.map((warning) => warning.message).toList(), + ); +} + +class _OneNoteConverter { + NoteData _document; + final String name; + + _OneNoteConverter(this.name) + : _document = DocumentDefaults.createDocument( + name: name, + createDefaultPage: false, + ); + + void addEntries(List entries, List path) { + for (final entry in entries) { + entry.when( + section: (section) => addSection(section, path), + sectionGroup: (group) => + addEntries(group.entries, [...path, group.displayName]), + ); + } + } + + void addSection(one.OneNoteSection section, List path) { + final sectionPath = [ + ...path, + section.displayName, + ].where((part) => part.trim().isNotEmpty).toList(); + for (final series in section.pageSeries) { + for (final page in series.pages) { + final builder = _OneNotePageBuilder(_document); + final converted = builder.convert(page); + _document = builder.document; + final title = page.title?.trim(); + final pageName = [ + ...sectionPath, + if (title != null && title.isNotEmpty) title else 'Untitled page', + ].join('/'); + final result = _document.addPage( + converted.copyWith( + extra: { + ...converted.extra, + 'onenote:section': section.displayName, + 'onenote:sectionPath': sectionPath, + 'onenote:pageId': page.linkTargetId, + 'onenote:pageLevel': page.level, + 'onenote:author': ?page.author, + 'onenote:createdAt': page.createdAt, + 'onenote:updatedAt': page.updatedAt, + 'onenote:recognizedText': ?page.recognizedText, + 'onenote:warnings': section.warnings + .where( + (warning) => + warning.pageId == null || + warning.pageId == page.linkTargetId, + ) + .map((warning) => warning.message) + .toList(), + }, + ), + pageName, + addNumber: false, + ); + _document = result.$1; + } + } + } + + NoteData finish({List warnings = const []}) { + final info = _document.getInfo() ?? const DocumentInfo(); + _document = _document.setInfo( + info.copyWith( + extra: { + ...info.extra, + 'onenote:imported': true, + 'onenote:warnings': warnings, + }, + ), + ); + return _document; + } +} + +class _OneNotePageBuilder { + NoteData document; + final List _elements = []; + final List _backgrounds = []; + + _OneNotePageBuilder(this.document); + + DocumentPage convert(one.OneNotePage page) { + for (final content in page.contents) { + content.when( + outline: _addOutline, + image: (image) => _addImage(image, const Point(0, 0)), + embeddedFile: (file) => _addEmbeddedFile(file, const Point(0, 0)), + ink: (ink) => _addInk(ink, const Point(0, 0), embedded: false), + unknown: () {}, + ); + } + return DocumentPage( + layers: [ + DocumentLayer( + name: 'OneNote', + content: _elements, + id: createUniqueId(), + ), + ], + backgrounds: _backgrounds.isEmpty + ? DocumentDefaults.createPage().backgrounds + : _backgrounds, + ); + } + + void _addOutline(one.OneNoteOutline outline) { + final outlineWidth = (outline.layoutMaxWidth ?? 13) * _pixelsPerHalfInch; + final origin = Point( + (outline.offsetHorizontal ?? 0) * _pixelsPerHalfInch, + (outline.offsetVertical ?? 0) * _pixelsPerHalfInch, + ); + var y = origin.y; + for (final item in outline.items) { + y = _addOutlineItem( + item, + Point(origin.x, y), + outlineWidth, + outline.indents, + 0, + outline.childLevel, + ); + } + } + + double _addOutlineItem( + one.OneNoteOutlineItem item, + Point position, + double outlineWidth, + Float32List indents, + int parentLevel, + int currentLevel, + ) { + return item.when( + group: (group) { + var y = position.y; + for (final child in group.items) { + y = _addOutlineItem( + child, + Point(position.x, y), + outlineWidth, + indents, + parentLevel, + currentLevel + group.childLevel, + ); + } + return y; + }, + element: (element) { + final indent = _outlineIndent(indents, parentLevel, currentLevel); + var y = position.y; + final x = position.x + indent; + final availableWidth = max(1.0, outlineWidth - indent); + for (final content in element.contents) { + y += _addContent(content, Point(x, y), availableWidth); + } + for (final child in element.children) { + y = _addOutlineItem( + child, + Point(position.x, y), + outlineWidth, + indents, + currentLevel, + currentLevel + element.childLevel, + ); + } + return y; + }, + ); + } + + double _addContent( + one.OneNoteContent content, + Point position, + double availableWidth, + ) { + return content.when( + richText: (value) => _addRichText(value, position, availableWidth), + table: (value) => _addTable(value, position, availableWidth), + image: (value) => _addImage(value, position), + embeddedFile: (value) => _addEmbeddedFile(value, position), + ink: (value) => _addInk(value, position, embedded: true), + unknown: () => 0, + ); + } + + double _addRichText( + one.OneNoteRichText value, + Point position, + double availableWidth, + ) { + final spaceBefore = value.paragraphSpaceBefore * _pixelsPerHalfInch; + final spaceAfter = value.paragraphSpaceAfter * _pixelsPerHalfInch; + if (value.text.isEmpty) { + return spaceBefore + spaceAfter; + } + final spans = _createSpans(value); + final paragraphStyle = value.paragraphStyle; + final fontSize = _fontSize( + value.textRunStyles.isEmpty + ? paragraphStyle + : value.textRunStyles.reduce( + (current, style) => + _fontSize(style) > _fontSize(current) ? style : current, + ), + ); + final explicitLines = '\n'.allMatches(value.text).length + 1; + final estimatedLineWidth = max(fontSize * 0.55, 1); + final wrappedLines = + (value.text.length * estimatedLineWidth / availableWidth).ceil(); + final lineCount = max(explicitLines, wrappedLines); + final height = + spaceBefore + spaceAfter + lineCount * max(fontSize * 1.2, 1); + _elements.add( + TextElement( + position: Point(position.x, position.y + spaceBefore), + area: text.TextArea( + paragraph: text.TextParagraph( + property: text.ParagraphProperty.defined( + alignment: _alignment(value.paragraphAlignment), + span: _spanProperty(paragraphStyle), + ), + textSpans: spans, + ), + ), + constraint: ElementConstraint( + size: availableWidth, + length: max(1, height - spaceBefore - spaceAfter), + includeArea: false, + ), + extra: { + 'onenote:paragraphSpaceBefore': spaceBefore, + 'onenote:paragraphSpaceAfter': spaceAfter, + 'onenote:font': ?paragraphStyle.font, + }, + ), + ); + return max(height, 16); + } + + List _createSpans(one.OneNoteRichText value) { + final indices = value.textRunIndices.map((index) => index.toInt()).toList(); + final styles = value.textRunStyles; + if (indices.isEmpty || styles.isEmpty) { + return [_inlineSpan(value.text, value.paragraphStyle)]; + } + + final spans = []; + var cursor = 0; + for (var i = 0; i < min(indices.length, styles.length); i++) { + final start = indices[i].clamp(cursor, value.text.length); + if (start > cursor) { + spans.add( + _inlineSpan( + value.text.substring(cursor, start), + value.paragraphStyle, + ), + ); + } + final end = i + 1 < indices.length + ? indices[i + 1].clamp(start, value.text.length) + : value.text.length; + if (end > start) { + spans.add(_inlineSpan(value.text.substring(start, end), styles[i])); + } + cursor = end; + } + if (cursor < value.text.length) { + spans.add( + _inlineSpan(value.text.substring(cursor), value.paragraphStyle), + ); + } + return spans; + } + + text.InlineSpan _inlineSpan(String value, one.OneNoteTextStyle style) { + final property = _spanProperty(style); + if (style.mathFormatting) { + return text.InlineSpan.math(text: value, property: property); + } + return text.InlineSpan.text(text: value, property: property); + } + + text.DefinedSpanProperty _spanProperty(one.OneNoteTextStyle style) => + text.DefinedSpanProperty( + size: _fontSize(style), + fontWeight: style.bold ? text.kFontWeightBold : text.kFontWeightNormal, + italic: style.italic, + underline: style.underline, + lineThrough: style.strikethrough, + ); + + double _addTable( + one.OneNoteTable table, + Point position, + double availableWidth, + ) { + final rows = table.rows + .map( + (row) => row.cells + .map( + (cell) => + cell.contents.map(_outlineElementText).join(' ').trim(), + ) + .join('\t'), + ) + .join('\n'); + if (rows.isEmpty) return 0; + final height = max(24.0, table.rowCount * 24.0); + final tableWidth = table.columnWidths.isEmpty + ? availableWidth + : min( + availableWidth, + table.columnWidths.fold(0, (sum, width) => sum + width) * + _pixelsPerHalfInch, + ); + _elements.add( + TextElement( + position: position, + area: text.TextArea( + paragraph: text.TextParagraph( + textSpans: [text.InlineSpan.text(text: rows)], + ), + ), + constraint: ElementConstraint( + size: tableWidth, + length: height, + includeArea: false, + ), + extra: { + 'onenote:tableRows': table.rowCount, + 'onenote:tableColumns': table.columnCount, + 'onenote:tableBordersVisible': table.bordersVisible, + 'onenote:columnWidths': table.columnWidths.toList(), + }, + ), + ); + return height; + } + + String _outlineElementText(one.OneNoteOutlineElement element) => [ + ...element.contents.map(_contentText), + ...element.children.map( + (child) => child.when( + group: (group) => group.items.map(_outlineItemText).join(' '), + element: _outlineElementText, + ), + ), + ].where((value) => value.trim().isNotEmpty).join(' '); + + String _outlineItemText(one.OneNoteOutlineItem item) => item.when( + group: (group) => group.items.map(_outlineItemText).join(' '), + element: _outlineElementText, + ); + + String _contentText(one.OneNoteContent content) => content.when( + richText: (value) => value.text, + table: (value) => value.rows + .map( + (row) => row.cells + .map((cell) => cell.contents.map(_outlineElementText).join(' ')) + .join(' '), + ) + .join(' '), + image: (value) => value.altText ?? value.ocrText ?? '', + embeddedFile: (value) => value.filename, + ink: (value) => _inkText(value), + unknown: () => '', + ); + + double _addImage(one.OneNoteImage image, Point fallback) { + final data = image.data; + if (data == null || data.isEmpty) { + final description = image.altText ?? image.ocrText; + if (description == null || description.isEmpty) return 0; + return _addPlainText( + description, + fallback, + extra: {'onenote:image': true}, + ); + } + final extension = _cleanExtension(image.extension_ ?? image.filename); + String path; + final imported = document.importImage(data, extension); + document = imported.$1; + path = imported.$2; + final width = + (image.pictureWidth ?? image.layoutMaxWidth ?? 4) * _pixelsPerHalfInch; + final height = + (image.pictureHeight ?? image.layoutMaxHeight ?? 3) * + _pixelsPerHalfInch; + final position = Point( + image.offsetHorizontal != null + ? image.offsetHorizontal! * _pixelsPerHalfInch + : fallback.x, + image.offsetVertical != null + ? image.offsetVertical! * _pixelsPerHalfInch + : fallback.y, + ); + final source = Uri.file(path, windows: false).toString(); + if (image.isBackground) { + _backgrounds.add( + Background.image( + source: source, + width: width, + height: height, + extra: { + 'onenote:filename': ?image.filename, + 'onenote:altText': ?image.altText, + 'onenote:ocrText': ?image.ocrText, + }, + ), + ); + } else { + _elements.add( + ImageElement( + source: source, + position: position, + width: width, + height: height, + extra: { + 'onenote:filename': ?image.filename, + 'onenote:altText': ?image.altText, + 'onenote:ocrText': ?image.ocrText, + 'onenote:hyperlink': ?image.hyperlinkUrl, + }, + ), + ); + } + return image.offsetHorizontal != null || image.offsetVertical != null + ? 0 + : height; + } + + double _addEmbeddedFile( + one.OneNoteEmbeddedFile file, + Point fallback, + ) { + final extension = _cleanExtension( + file.fileType.isNotEmpty ? file.fileType : file.filename, + ); + String path; + final imported = document.importAsset('attachments', file.data, extension); + document = imported.$1; + path = imported.$2; + final position = Point( + file.offsetHorizontal != null + ? file.offsetHorizontal! * _pixelsPerHalfInch + : fallback.x, + file.offsetVertical != null + ? file.offsetVertical! * _pixelsPerHalfInch + : fallback.y, + ); + final height = _addPlainText( + '📎 ${file.filename}', + position, + extra: { + 'onenote:attachment': path, + 'onenote:fileType': file.fileType, + 'onenote:size': file.size.toString(), + }, + ); + return file.offsetHorizontal != null || file.offsetVertical != null + ? 0 + : height; + } + + double _addInk( + one.OneNoteInk ink, + Point fallback, { + required bool embedded, + }) { + final maxY = _addInkAt(ink, fallback, embedded, ink.boundingBox); + return max(0, maxY - fallback.y); + } + + double _addInkAt( + one.OneNoteInk ink, + Point fallback, + bool embedded, + one.OneNoteInkBoundingBox? displayBoundingBox, + ) { + final boundingBox = ink.boundingBox ?? displayBoundingBox; + final origin = embedded + ? Point( + fallback.x - (boundingBox?.x ?? 0) * _himetricToPixels, + fallback.y - (boundingBox?.y ?? 0) * _himetricToPixels, + ) + : Point( + fallback.x + (ink.offsetHorizontal ?? 0) * _pixelsPerHalfInch, + fallback.y + (ink.offsetVertical ?? 0) * _pixelsPerHalfInch, + ); + var maxY = embedded && boundingBox != null + ? fallback.y + boundingBox.height * _himetricToPixels + : origin.y; + for (final stroke in ink.strokes) { + final decoded = _decodeInkPath(stroke.path); + if (decoded.isEmpty) continue; + final color = _inkColor(stroke.color, stroke.transparency); + final points = decoded + .map( + (point) => PathPoint( + origin.x + point.x * _himetricToPixels, + origin.y + point.y * _himetricToPixels, + ), + ) + .toList(); + maxY = max(maxY, points.map((point) => point.y).reduce(max)); + _elements.add( + PenElement( + points: points, + property: PenProperty( + strokeWidth: _inkStrokeWidth(stroke), + thinning: 0, + smoothing: 0, + streamline: 0, + paint: ElementPaint.solid(color: color), + ), + extra: { + 'onenote:penTip': ?stroke.penTip, + 'onenote:embeddedInk': embedded, + 'onenote:inkWidth': stroke.width, + 'onenote:inkHeight': stroke.height, + 'onenote:transparency': ?stroke.transparency, + 'onenote:recognizedText': ?stroke.recognizedText, + }, + ), + ); + } + for (final child in ink.childGroups) { + maxY = max( + maxY, + _addInkAt( + child, + fallback, + embedded, + ink.boundingBox ?? displayBoundingBox, + ), + ); + } + return maxY; + } + + double _addPlainText( + String value, + Point position, { + Map extra = const {}, + }) { + const height = 24.0; + _elements.add( + TextElement( + position: position, + area: text.TextArea( + paragraph: text.TextParagraph( + textSpans: [text.InlineSpan.text(text: value)], + ), + ), + constraint: const ElementConstraint(size: height), + extra: extra, + ), + ); + return height; + } + + String _inkText(one.OneNoteInk ink) => [ + ...ink.strokes.map((stroke) => stroke.recognizedText ?? ''), + ...ink.childGroups.map(_inkText), + ].where((value) => value.isNotEmpty).join(' '); + + text.HorizontalAlignment _alignment(String value) { + final normalized = value.toLowerCase(); + if (normalized.contains('center')) return text.HorizontalAlignment.center; + if (normalized.contains('right')) return text.HorizontalAlignment.right; + if (normalized.contains('justify')) { + return text.HorizontalAlignment.justify; + } + return text.HorizontalAlignment.left; + } + + double _fontSize(one.OneNoteTextStyle style) => + ((style.fontSize ?? 18) / 2 * _pixelsPerInch / 72).clamp(1, 512); + + SRGBColor _inkColor(int? value, int? transparency) { + final color = value ?? 0; + final red = color & 0xFF; + final green = (color >> 8) & 0xFF; + final blue = (color >> 16) & 0xFF; + final alpha = 255 - (transparency ?? 0).clamp(0, 255); + return SRGBColor((alpha << 24) | (red << 16) | (green << 8) | blue); + } + + double _inkStrokeWidth(one.OneNoteInkStroke stroke) { + final width = stroke.penTip == 1 + ? max(stroke.width, stroke.height) + : stroke.width; + return width.abs() * _himetricToPixels; + } + + List> _decodeInkPath(List encoded) { + if (encoded.isEmpty) return const []; + var x = encoded.first.x; + var y = encoded.first.y; + final points = >[Point(x, y)]; + for (var i = 1; i < encoded.length; i++) { + x += encoded[i].x; + y += encoded[i].y; + points.add(Point(x, y)); + } + return points; + } + + String _cleanExtension(String? value) { + final extension = + value?.split('/').last.split('.').last.toLowerCase() ?? ''; + final cleaned = extension.replaceAll(RegExp('[^a-z0-9]'), ''); + return cleaned.isEmpty ? 'bin' : cleaned; + } + + double _outlineIndent( + Float32List indents, + int parentLevel, + int currentLevel, + ) { + var width = 0.0; + for (var level = parentLevel + 1; level <= currentLevel; level++) { + width += + (level < indents.length ? indents[level] : 0.75) * _pixelsPerHalfInch; + } + return width; + } +} diff --git a/app/lib/visualizer/asset.dart b/app/lib/visualizer/asset.dart index 4cb28c4a8d4f..9860d1d1e147 100644 --- a/app/lib/visualizer/asset.dart +++ b/app/lib/visualizer/asset.dart @@ -13,6 +13,7 @@ extension AssetFileTypeVisualizer on AssetFileType? { AssetFileType.pdf => AppLocalizations.of(context).pdf, AssetFileType.svg => AppLocalizations.of(context).svg, AssetFileType.xopp => 'Xournal++', + AssetFileType.oneNote || AssetFileType.oneNotePackage => 'OneNote', AssetFileType.page => AppLocalizations.of(context).page, AssetFileType.archive => AppLocalizations.of(context).data, AssetFileType.rawText => AppLocalizations.of(context).text, @@ -26,6 +27,8 @@ extension AssetFileTypeVisualizer on AssetFileType? { AssetFileType.pdf => PhosphorIcons.filePdf, AssetFileType.svg => PhosphorIcons.fileSvg, AssetFileType.xopp => PhosphorIcons.notebook, + AssetFileType.oneNote || + AssetFileType.oneNotePackage => PhosphorIcons.notebook, AssetFileType.page => PhosphorIcons.book, AssetFileType.archive => PhosphorIcons.archive, _ => PhosphorIcons.file, diff --git a/app/linux/debian/usr/share/applications/dev.linwood.butterfly.desktop b/app/linux/debian/usr/share/applications/dev.linwood.butterfly.desktop index ba02d77e0b48..a0c8a5012bc4 100644 --- a/app/linux/debian/usr/share/applications/dev.linwood.butterfly.desktop +++ b/app/linux/debian/usr/share/applications/dev.linwood.butterfly.desktop @@ -6,4 +6,4 @@ Icon=dev.linwood.butterfly Terminal=false Type=Application Categories=Office; -MimeType=application/x-butterfly;application/x-text-butterfly;image/bmp;image/gif;image/jpeg;image/jpg;image/png;image/svg+xml;image/svg+xml-compressed;image/x-ico;application/pdf; \ No newline at end of file +MimeType=application/x-butterfly;application/x-text-butterfly;application/onenote;image/bmp;image/gif;image/jpeg;image/jpg;image/png;image/svg+xml;image/svg+xml-compressed;image/x-ico;application/pdf; diff --git a/app/linux/debian/usr/share/mime/packages/dev.linwood.butterfly.xml b/app/linux/debian/usr/share/mime/packages/dev.linwood.butterfly.xml index d7b50b9651dd..0e7acab5f67d 100644 --- a/app/linux/debian/usr/share/mime/packages/dev.linwood.butterfly.xml +++ b/app/linux/debian/usr/share/mime/packages/dev.linwood.butterfly.xml @@ -12,4 +12,10 @@ Butterfly text files - \ No newline at end of file + + + Microsoft OneNote files + + + + diff --git a/app/linux/rpm/linwood-butterfly.desktop b/app/linux/rpm/linwood-butterfly.desktop index 995a19b1f6ef..da8e501fd33a 100644 --- a/app/linux/rpm/linwood-butterfly.desktop +++ b/app/linux/rpm/linwood-butterfly.desktop @@ -6,4 +6,4 @@ Icon=/usr/share/linwood-butterfly/data/flutter_assets/images/logo.svg Terminal=false Type=Application Categories=Office; -MimeType=application/x-butterfly;application/x-text-butterfly;image/bmp;image/gif;image/jpeg;image/jpg;image/png;image/svg+xml;image/svg+xml-compressed;image/x-ico;application/pdf; \ No newline at end of file +MimeType=application/x-butterfly;application/x-text-butterfly;application/onenote;image/bmp;image/gif;image/jpeg;image/jpg;image/png;image/svg+xml;image/svg+xml-compressed;image/x-ico;application/pdf; diff --git a/app/pubspec.lock b/app/pubspec.lock index 122c2d65ca6d..6e0282e7006d 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -97,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.6" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95 + url: "https://pub.dev" + source: hosted + version: "2.1.1" build_config: dependency: transitive description: @@ -228,10 +236,10 @@ packages: dependency: transitive description: name: code_assets - sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.0.0" collection: dependency: "direct main" description: @@ -472,6 +480,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.35" + flutter_rust_bridge: + dependency: transitive + description: + name: flutter_rust_bridge + sha256: "37bcf055414b4b6417a046d536c16d09a1f58bdf099f45a91008f0fe49f641c0" + url: "https://pub.dev" + source: hosted + version: "2.13.0-beta.2" + flutter_rust_bridge_hooks: + dependency: transitive + description: + name: flutter_rust_bridge_hooks + sha256: bebf66b09522335b99a77728fd9856ac76bd40e66431db21f01307a54b78f0f9 + url: "https://pub.dev" + source: hosted + version: "2.13.0-beta.2" flutter_secure_storage: dependency: "direct main" description: @@ -603,10 +627,10 @@ packages: dependency: transitive description: name: hooks - sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" + sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "1.0.3" html: dependency: "direct main" description: @@ -849,6 +873,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.17.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + url: "https://pub.dev" + source: hosted + version: "0.17.6" + native_toolchain_rust: + dependency: transitive + description: + name: native_toolchain_rust + sha256: "26d4dcae954328af4ebfa8fc56cca0bf74b9ae8b4a6132de5da2071ac3d237b3" + url: "https://pub.dev" + source: hosted + version: "1.0.4" nested: dependency: transitive description: @@ -912,10 +952,10 @@ packages: dependency: transitive description: name: objective_c - sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" url: "https://pub.dev" source: hosted - version: "9.4.1" + version: "9.3.0" one_dollar_unistroke_recognizer: dependency: "direct main" description: @@ -924,6 +964,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.4" + onenote_parser: + dependency: "direct main" + description: + path: "packages/onenote_parser" + ref: a21d414a0cb287b6112f3eb7917c19181be96638 + resolved-ref: a21d414a0cb287b6112f3eb7917c19181be96638 + url: "https://github.com/LinwoodDev/dart_pkgs.git" + source: git + version: "0.0.1" package_config: dependency: transitive description: @@ -1451,6 +1500,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + toml: + dependency: transitive + description: + name: toml + sha256: "35a35f782228656a2af31e8c73d1353cc4ef3d683fd68af1111b44631879c05e" + url: "https://pub.dev" + source: hosted + version: "0.18.0" tuple: dependency: transitive description: @@ -1668,5 +1725,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.12.0 <4.0.0" + dart: ">=3.12.2 <4.0.0" flutter: "3.44.2" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 493914e30029..e625182ec4ae 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: none version: 2.6.0-beta.1+187 environment: - sdk: ">=3.9.0 <4.0.0" + sdk: ">=3.12.2 <4.0.0" flutter: 3.44.2 dependencies: @@ -106,6 +106,11 @@ dependencies: markdown: ^7.2.2 image: ^4.1.7 one_dollar_unistroke_recognizer: ^1.2.0 + onenote_parser: + git: + url: https://github.com/LinwoodDev/dart_pkgs.git + ref: a21d414a0cb287b6112f3eb7917c19181be96638 + path: packages/onenote_parser web: ^1.0.0 cryptography_plus: ^3.0.0 barcode: ^2.2.9 @@ -243,5 +248,5 @@ msix_config: logo_path: images/logo.png capabilities: internetClientServer, webcam, documentsLibrary languages: en-us, af, ar, ca, cs, da, de, el, es, fi, fr, he, hi, hu, id, it, ja, ko, nl, no, pl, pt-br, pt, ro, ru, sr-Latn, sv, th, tr, uk, vi, zh-Hans, zh-Hant - file_extension: .bfly, .tbfly, .pdf, .jpg, .jpeg, .png, .gif, .bmp, .ico, .md + file_extension: .bfly, .tbfly, .pdf, .jpg, .jpeg, .png, .gif, .bmp, .ico, .md, .one, .onepkg install_certificate: false diff --git a/app/test/services/onenote_test.dart b/app/test/services/onenote_test.dart new file mode 100644 index 000000000000..61f8e70ee4b7 --- /dev/null +++ b/app/test/services/onenote_test.dart @@ -0,0 +1,460 @@ +import 'dart:typed_data'; + +import 'package:butterfly/services/onenote.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:butterfly_api/butterfly_text.dart' as text; +import 'package:flutter_test/flutter_test.dart'; +import 'package:onenote_parser/onenote_parser.dart' as one; + +const _plainStyle = one.OneNoteTextStyle( + bold: false, + italic: false, + underline: false, + strikethrough: false, + superscript: false, + subscript: false, + fontSize: 12, + mathFormatting: false, + hyperlink: false, + hyperlinkProtected: false, + hidden: false, +); + +const _boldStyle = one.OneNoteTextStyle( + bold: true, + italic: false, + underline: false, + strikethrough: false, + superscript: false, + subscript: false, + fontSize: 16, + mathFormatting: false, + hyperlink: false, + hyperlinkProtected: false, + hidden: false, +); + +void main() { + test('converts OneNote page content and embedded assets', () { + final richText = one.OneNoteRichText( + text: 'Hello world', + textRunIndices: Uint32List.fromList([0, 6]), + textRunStyles: const [_boldStyle, _plainStyle], + paragraphStyle: _plainStyle, + paragraphSpaceBefore: 0.25, + paragraphSpaceAfter: 0.5, + paragraphAlignment: 'Center', + ); + final outline = one.OneNoteOutline( + childLevel: 0, + indents: Float32List(0), + isLayoutSizeSetByUser: true, + layoutMaxWidth: 6, + offsetHorizontal: 1, + offsetVertical: 2, + items: [ + one.OneNoteOutlineItem.element( + one.OneNoteOutlineElement( + contents: [one.OneNoteContent.richText(richText)], + childLevel: 0, + children: const [], + ), + ), + ], + ); + final imageBytes = Uint8List.fromList([1, 2, 3]); + final attachmentBytes = Uint8List.fromList([4, 5, 6]); + final page = one.OneNotePage( + linkTargetId: 'page-id', + title: 'Planning', + level: 1, + createdAt: '2026-01-02T03:04:05Z', + updatedAt: '2026-01-03T03:04:05Z', + author: 'Ada', + contents: [ + one.OneNotePageContent.outline(outline), + one.OneNotePageContent.image( + one.OneNoteImage( + data: imageBytes, + extension_: 'png', + filename: 'diagram.png', + pictureWidth: 4, + pictureHeight: 3, + offsetHorizontal: 2, + offsetVertical: 4, + isBackground: false, + ), + ), + one.OneNotePageContent.embeddedFile( + one.OneNoteEmbeddedFile( + filename: 'notes.txt', + fileType: 'txt', + data: attachmentBytes, + size: BigInt.from(attachmentBytes.length), + offsetHorizontal: 0.5, + offsetVertical: 8, + ), + ), + one.OneNotePageContent.ink( + const one.OneNoteInk( + strokes: [ + one.OneNoteInkStroke( + path: [ + one.OneNoteInkPoint(x: 1, y: 2), + one.OneNoteInkPoint(x: 3, y: 4), + ], + transparency: 64, + height: 100, + width: 100, + color: 0x112233, + ), + ], + childGroups: [], + offsetHorizontal: 10, + offsetVertical: 20, + ), + ), + ], + ); + final section = one.OneNoteSection( + displayName: 'Work', + pageSeries: [ + one.OneNotePageSeries(pages: [page]), + ], + warnings: const [], + ); + + final document = convertOneNoteSection(section, name: 'Imported'); + final convertedPage = document.getPage('Work/Planning'); + + expect(document.getMetadata()?.name, 'Imported'); + expect(convertedPage, isNotNull); + expect(convertedPage!.extra['onenote:author'], 'Ada'); + + final label = convertedPage.content.whereType().first; + expect(label.position.x, 48); + expect(label.position.y, 108); + expect(label.constraint.size, 288); + expect(label.constraint.length, closeTo(12.8, 0.0001)); + expect( + (label.area.paragraph.property as text.DefinedParagraphProperty) + .alignment, + text.HorizontalAlignment.center, + ); + expect(label.area.paragraph.textSpans, hasLength(2)); + expect( + (label.area.paragraph.textSpans.first.property + as text.DefinedSpanProperty) + .fontWeight, + text.kFontWeightBold, + ); + + final image = convertedPage.content.whereType().single; + expect(image.position.x, 96); + expect(image.position.y, 192); + expect(image.width, 192); + expect(image.height, 144); + expect(document.getAsset(Uri.parse(image.source).path), imageBytes); + + final attachment = convertedPage.content + .whereType() + .firstWhere((element) => element.extra['onenote:attachment'] != null); + final attachmentPath = attachment.extra['onenote:attachment'] as String; + expect(document.getAsset(attachmentPath), attachmentBytes); + + final stroke = convertedPage.content.whereType().single; + expect(stroke.points.first.x, closeTo(480.0378, 0.0001)); + expect(stroke.points.first.y, closeTo(960.0756, 0.0001)); + expect(stroke.property.strokeWidth, closeTo(3.7795, 0.0001)); + expect(stroke.property.thinning, 0); + expect(stroke.property.streamline, 0); + expect(stroke.property.paint.previewColor.value, 0xBF332211); + }); + + test('uses leaf offsets for nested page ink', () { + const stroke = one.OneNoteInkStroke( + path: [one.OneNoteInkPoint(x: 2540, y: 1270)], + penTip: 0, + height: 400, + width: 100, + ); + const ink = one.OneNoteInk( + strokes: [], + childGroups: [ + one.OneNoteInk( + strokes: [stroke], + childGroups: [], + offsetHorizontal: 2, + offsetVertical: 3, + ), + ], + offsetHorizontal: 1, + offsetVertical: 1, + ); + final section = one.OneNoteSection( + displayName: 'Ink', + pageSeries: [ + one.OneNotePageSeries( + pages: [ + one.OneNotePage( + linkTargetId: 'ink-page', + title: 'Nested', + level: 0, + createdAt: '', + updatedAt: '', + contents: [one.OneNotePageContent.ink(ink)], + ), + ], + ), + ], + warnings: const [], + ); + + final document = convertOneNoteSection(section); + final imported = document + .getPage('Ink/Nested')! + .content + .whereType() + .single; + + expect(imported.points.single.x, 192); + expect(imported.points.single.y, 192); + expect(imported.property.strokeWidth, closeTo(3.7795, 0.0001)); + }); + + test('positions embedded ink relative to its bounding box', () { + const ink = one.OneNoteInk( + strokes: [ + one.OneNoteInkStroke( + path: [one.OneNoteInkPoint(x: 2540, y: 5080)], + height: 100, + width: 100, + ), + ], + childGroups: [], + boundingBox: one.OneNoteInkBoundingBox( + x: 2540, + y: 5080, + width: 1000, + height: 1000, + ), + offsetHorizontal: 20, + offsetVertical: 20, + ); + final outline = one.OneNoteOutline( + childLevel: 0, + indents: Float32List(0), + isLayoutSizeSetByUser: true, + layoutMaxWidth: 5, + offsetHorizontal: 2, + offsetVertical: 3, + items: [ + one.OneNoteOutlineItem.element( + const one.OneNoteOutlineElement( + contents: [one.OneNoteContent.ink(ink)], + childLevel: 0, + children: [], + ), + ), + ], + ); + final section = one.OneNoteSection( + displayName: 'Ink', + pageSeries: [ + one.OneNotePageSeries( + pages: [ + one.OneNotePage( + linkTargetId: 'embedded-page', + title: 'Embedded', + level: 0, + createdAt: '', + updatedAt: '', + contents: [one.OneNotePageContent.outline(outline)], + ), + ], + ), + ], + warnings: const [], + ); + + final document = convertOneNoteSection(section); + final point = document + .getPage('Ink/Embedded')! + .content + .whereType() + .single + .points + .single; + + expect(point.x, 96); + expect(point.y, 144); + }); + + test('reserves embedded ink bounding box height in outline flow', () { + const ink = one.OneNoteInk( + strokes: [ + one.OneNoteInkStroke( + path: [one.OneNoteInkPoint(x: 0, y: 0)], + height: 100, + width: 100, + ), + ], + childGroups: [], + boundingBox: one.OneNoteInkBoundingBox( + x: 0, + y: 0, + width: 2540, + height: 2540, + ), + ); + final outline = one.OneNoteOutline( + childLevel: 0, + indents: Float32List(0), + isLayoutSizeSetByUser: true, + layoutMaxWidth: 5, + items: [ + one.OneNoteOutlineItem.element( + one.OneNoteOutlineElement( + contents: [ + const one.OneNoteContent.ink(ink), + one.OneNoteContent.richText( + one.OneNoteRichText( + text: 'After ink', + textRunIndices: Uint32List(0), + textRunStyles: const [], + paragraphStyle: _plainStyle, + paragraphSpaceBefore: 0, + paragraphSpaceAfter: 0, + paragraphAlignment: 'Left', + ), + ), + ], + childLevel: 0, + children: const [], + ), + ), + ], + ); + final section = one.OneNoteSection( + displayName: 'Ink', + pageSeries: [ + one.OneNotePageSeries( + pages: [ + one.OneNotePage( + linkTargetId: 'flow-page', + title: 'Flow', + level: 0, + createdAt: '', + updatedAt: '', + contents: [one.OneNotePageContent.outline(outline)], + ), + ], + ), + ], + warnings: const [], + ); + + final document = convertOneNoteSection(section); + final label = document + .getPage('Ink/Flow')! + .content + .whereType() + .single; + + expect(label.position.y, 96); + }); + + test('reconstructs OneNote delta ink packets', () { + const ink = one.OneNoteInk( + strokes: [ + one.OneNoteInkStroke( + path: [ + one.OneNoteInkPoint(x: 2540, y: 2540), + one.OneNoteInkPoint(x: 254, y: 0), + one.OneNoteInkPoint(x: 0, y: 254), + one.OneNoteInkPoint(x: -127, y: 0), + ], + height: 100, + width: 100, + ), + ], + childGroups: [], + ); + final section = one.OneNoteSection( + displayName: 'Ink', + pageSeries: [ + one.OneNotePageSeries( + pages: [ + one.OneNotePage( + linkTargetId: 'delta-page', + title: 'Delta', + level: 0, + createdAt: '', + updatedAt: '', + contents: [one.OneNotePageContent.ink(ink)], + ), + ], + ), + ], + warnings: const [], + ); + + final document = convertOneNoteSection(section); + final points = document + .getPage('Ink/Delta')! + .content + .whereType() + .single + .points; + + expect(points.map((point) => point.x), [ + closeTo(96, 0.0001), + closeTo(105.6, 0.0001), + closeTo(105.6, 0.0001), + closeTo(100.8, 0.0001), + ]); + expect(points.map((point) => point.y), [ + closeTo(96, 0.0001), + closeTo(96, 0.0001), + closeTo(105.6, 0.0001), + closeTo(105.6, 0.0001), + ]); + }); + + test('keeps notebook section groups in page names', () { + final page = one.OneNotePage( + linkTargetId: 'page-id', + title: 'Ideas', + level: 0, + createdAt: '', + updatedAt: '', + contents: const [], + ); + final notebook = one.OneNoteNotebook( + entries: [ + one.OneNoteSectionEntry.sectionGroup( + one.OneNoteSectionGroup( + displayName: 'Projects', + entries: [ + one.OneNoteSectionEntry.section( + one.OneNoteSection( + displayName: 'Butterfly', + pageSeries: [ + one.OneNotePageSeries(pages: [page]), + ], + warnings: const [], + ), + ), + ], + ), + ), + ], + warnings: const [], + ); + + final document = convertOneNoteNotebook(notebook, name: 'Notebook'); + + expect(document.getPages(), ['Projects/Butterfly/Ideas']); + expect(document.getInfo()?.extra['onenote:imported'], isTrue); + }); +} From b98029c32ac1e3347170d61fc52872c1e30b0d30 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 21 Jun 2026 19:53:49 +0200 Subject: [PATCH 003/117] Fix stroke and embedded objects onenote imports --- app/lib/services/onenote.dart | 100 ++++++++++++++++++++++++++-- app/pubspec.lock | 4 +- app/pubspec.yaml | 2 +- app/test/services/onenote_test.dart | 2 + 4 files changed, 99 insertions(+), 9 deletions(-) diff --git a/app/lib/services/onenote.dart b/app/lib/services/onenote.dart index b8f8e03e2c43..43f3c891e6cd 100644 --- a/app/lib/services/onenote.dart +++ b/app/lib/services/onenote.dart @@ -246,6 +246,20 @@ class _OneNotePageBuilder { ) { final spaceBefore = value.paragraphSpaceBefore * _pixelsPerHalfInch; final spaceAfter = value.paragraphSpaceAfter * _pixelsPerHalfInch; + + if (value.embeddedObjects.isNotEmpty) { + final height = _addEmbeddedObjects( + value.embeddedObjects, + Point(position.x, position.y + spaceBefore), + availableWidth, + ); + return spaceBefore + height + spaceAfter; + } + + if (value.text.isEmpty) { + return spaceBefore + spaceAfter; + } + if (value.text.isEmpty) { return spaceBefore + spaceAfter; } @@ -293,6 +307,71 @@ class _OneNotePageBuilder { return max(height, 16); } + double _addEmbeddedObjects( + List objects, + Point origin, + double availableWidth, + ) { + var x = origin.x; + var y = origin.y; + var lineHeight = 0.0; + var maximumY = origin.y; + + void newLine() { + y += max(lineHeight, 16); + x = origin.x; + lineHeight = 0; + } + + for (final object in objects) { + object.when( + ink: (embedded) { + final inkBox = embedded.ink.boundingBox; + final displayBox = embedded.displayBoundingBox; + + // InkBoundingBox is in HIMETRIC. + // Embedded display dimensions are already display/pixel-like units. + final width = inkBox != null + ? inkBox.width * _himetricToPixels + : displayBox?.width ?? 0; + + final height = inkBox != null + ? inkBox.height * _himetricToPixels + : displayBox?.height ?? 0; + + if (x > origin.x && + availableWidth.isFinite && + x + width > origin.x + availableWidth) { + newLine(); + } + + _addInkAt( + embedded.ink, + Point(x, y), + true, + displayBox, + displayBoundingBoxScale: 1, + ); + + x += width; + lineHeight = max(lineHeight, height); + maximumY = max(maximumY, y + height); + }, + inkSpace: (space) { + // These values use OneNote half-inch layout units. + x += space.width * _pixelsPerHalfInch; + lineHeight = max(lineHeight, space.height * _pixelsPerHalfInch); + }, + inkLineBreak: () { + newLine(); + }, + ); + } + + maximumY = max(maximumY, y + lineHeight); + return max(16, maximumY - origin.y); + } + List _createSpans(one.OneNoteRichText value) { final indices = value.textRunIndices.map((index) => index.toInt()).toList(); final styles = value.textRunStyles; @@ -533,21 +612,30 @@ class _OneNotePageBuilder { one.OneNoteInk ink, Point fallback, bool embedded, - one.OneNoteInkBoundingBox? displayBoundingBox, - ) { - final boundingBox = ink.boundingBox ?? displayBoundingBox; + one.OneNoteInkBoundingBox? displayBoundingBox, { + double displayBoundingBoxScale = _himetricToPixels, + }) { + final inkBoundingBox = ink.boundingBox; + final boundingBox = inkBoundingBox ?? displayBoundingBox; + + final boundingBoxScale = inkBoundingBox != null + ? _himetricToPixels + : displayBoundingBoxScale; + final origin = embedded ? Point( - fallback.x - (boundingBox?.x ?? 0) * _himetricToPixels, - fallback.y - (boundingBox?.y ?? 0) * _himetricToPixels, + fallback.x - (boundingBox?.x ?? 0) * boundingBoxScale, + fallback.y - (boundingBox?.y ?? 0) * boundingBoxScale, ) : Point( fallback.x + (ink.offsetHorizontal ?? 0) * _pixelsPerHalfInch, fallback.y + (ink.offsetVertical ?? 0) * _pixelsPerHalfInch, ); + var maxY = embedded && boundingBox != null - ? fallback.y + boundingBox.height * _himetricToPixels + ? fallback.y + boundingBox.height * boundingBoxScale : origin.y; + for (final stroke in ink.strokes) { final decoded = _decodeInkPath(stroke.path); if (decoded.isEmpty) continue; diff --git a/app/pubspec.lock b/app/pubspec.lock index 6e0282e7006d..01fff8aa9c0f 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -968,8 +968,8 @@ packages: dependency: "direct main" description: path: "packages/onenote_parser" - ref: a21d414a0cb287b6112f3eb7917c19181be96638 - resolved-ref: a21d414a0cb287b6112f3eb7917c19181be96638 + ref: "5d2919c34bcd128bc904a1cfe18b03cb37f02006" + resolved-ref: "5d2919c34bcd128bc904a1cfe18b03cb37f02006" url: "https://github.com/LinwoodDev/dart_pkgs.git" source: git version: "0.0.1" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index e625182ec4ae..f5106e53a1ec 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -109,7 +109,7 @@ dependencies: onenote_parser: git: url: https://github.com/LinwoodDev/dart_pkgs.git - ref: a21d414a0cb287b6112f3eb7917c19181be96638 + ref: 5d2919c34bcd128bc904a1cfe18b03cb37f02006 path: packages/onenote_parser web: ^1.0.0 cryptography_plus: ^3.0.0 diff --git a/app/test/services/onenote_test.dart b/app/test/services/onenote_test.dart index 61f8e70ee4b7..dd9c4215d941 100644 --- a/app/test/services/onenote_test.dart +++ b/app/test/services/onenote_test.dart @@ -44,6 +44,7 @@ void main() { paragraphSpaceBefore: 0.25, paragraphSpaceAfter: 0.5, paragraphAlignment: 'Center', + embeddedObjects: const [], ); final outline = one.OneNoteOutline( childLevel: 0, @@ -326,6 +327,7 @@ void main() { paragraphSpaceBefore: 0, paragraphSpaceAfter: 0, paragraphAlignment: 'Left', + embeddedObjects: const [], ), ), ], From 22a98952696df1e7a77a34a86a2eeae3ab8187b3 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 21 Jun 2026 22:00:54 +0200 Subject: [PATCH 004/117] Fix xps inside onenote --- app/lib/services/import.dart | 163 +++++++++++++++++ app/lib/services/onenote.dart | 263 ++++++++++++++++++++++++++-- app/test/services/onenote_test.dart | 193 ++++++++++++++++++++ 3 files changed, 602 insertions(+), 17 deletions(-) diff --git a/app/lib/services/import.dart b/app/lib/services/import.dart index 2af3fa35b331..1e393bb713ac 100644 --- a/app/lib/services/import.dart +++ b/app/lib/services/import.dart @@ -18,6 +18,7 @@ import 'package:butterfly/models/defaults.dart'; import 'package:butterfly/renderers/renderer.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:collection/collection.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -36,6 +37,10 @@ import '../dialogs/import/pages.dart'; import '../dialogs/export/pdf.dart'; import 'onenote.dart'; +enum _OneNoteXpsFallback { manual, skipAll } + +enum _OneNoteManualXpsAction { selectPdf, exportAgain, skipFile, skipAll } + class ImportResult { final ImportService service; final NoteData? document; @@ -664,6 +669,163 @@ class ImportService { String? name, }) async { try { + var manuallyConvertXps = false; + var skipRemainingXps = false; + + Future convertXps(Uint8List data, String fileName) async { + if (skipRemainingXps) return null; + if (!manuallyConvertXps) { + late Object conversionError; + try { + return await convertXpsToPdf(data); + } catch (error) { + conversionError = error; + } + if (!context.mounted) return null; + final executableMissing = + conversionError is XpsToPdfNotInstalledException; + final fallback = await showDialog<_OneNoteXpsFallback>( + context: context, + builder: (context) => AlertDialog( + title: Text( + executableMissing + ? 'XPS converter not found' + : 'Automatic XPS conversion failed', + ), + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 520), + child: Text( + '${executableMissing ? 'The xpstopdf command is not installed or cannot be started.' : 'xpstopdf could not convert “$fileName”.'}\n\n' + 'OneNote stores printed documents as XPS files. Butterfly ' + 'needs a PDF version to display these printouts.\n\n' + 'Choose “Convert manually” to:\n' + '1. Export the original XPS file.\n' + '2. Convert it to PDF with an application of your choice.\n' + '3. Select the converted PDF and continue importing.\n\n' + 'You can also skip every XPS printout and import the rest ' + 'of the notebook.', + ), + ), + actions: [ + TextButton( + onPressed: () => + Navigator.pop(context, _OneNoteXpsFallback.skipAll), + child: const Text('Skip all XPS files'), + ), + FilledButton( + onPressed: () => + Navigator.pop(context, _OneNoteXpsFallback.manual), + child: const Text('Convert manually'), + ), + ], + ), + ); + manuallyConvertXps = fallback == _OneNoteXpsFallback.manual; + if (!manuallyConvertXps) { + skipRemainingXps = true; + return null; + } + } + + final baseName = p + .basenameWithoutExtension(fileName) + .replaceAll(RegExp(r'[^\w.-]'), '_'); + final safeName = baseName.isEmpty ? 'printout' : baseName; + var exported = false; + String? message; + while (context.mounted) { + final action = await showDialog<_OneNoteManualXpsAction>( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: Text('Convert “$fileName” to PDF'), + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 520), + child: Text( + '${message == null ? '' : '$message\n\n'}' + '${exported ? 'The XPS export has been opened.' : 'Start by exporting the original XPS file.'}\n\n' + '1. Save “$safeName.xps” somewhere you can find it.\n' + '2. Convert that file to PDF outside Butterfly.\n' + '3. Return here and choose “Select converted PDF”.\n\n' + 'Canceling a file picker returns you to this dialog. ' + 'Skipping this file does not cancel the rest of the ' + 'OneNote import.', + ), + ), + actions: [ + TextButton( + onPressed: () => + Navigator.pop(context, _OneNoteManualXpsAction.skipAll), + child: const Text('Skip all XPS files'), + ), + TextButton( + onPressed: () => + Navigator.pop(context, _OneNoteManualXpsAction.skipFile), + child: const Text('Skip this file'), + ), + OutlinedButton( + onPressed: () => Navigator.pop( + context, + _OneNoteManualXpsAction.exportAgain, + ), + child: Text(exported ? 'Export XPS again' : 'Export XPS'), + ), + FilledButton( + onPressed: exported + ? () => Navigator.pop( + context, + _OneNoteManualXpsAction.selectPdf, + ) + : null, + child: const Text('Select converted PDF'), + ), + ], + ), + ); + switch (action) { + case _OneNoteManualXpsAction.exportAgain: + await exportFile( + context: context, + bytes: data, + fileName: safeName, + fileExtension: 'xps', + mimeType: 'application/oxps', + uniformTypeIdentifier: 'com.microsoft.xps', + label: 'Export XPS for manual conversion', + ); + exported = true; + message = null; + continue; + case _OneNoteManualXpsAction.selectPdf: + final converted = await FilePicker.pickFile( + dialogTitle: 'Select the PDF converted from $fileName', + type: FileType.custom, + allowedExtensions: const ['pdf'], + ); + if (converted == null) { + message = 'No PDF was selected.'; + continue; + } + final convertedData = await converted.readAsBytes(); + if (convertedData.length < 5 || + String.fromCharCodes(convertedData.take(5)) != '%PDF-') { + message = + 'The selected file does not appear to be a valid PDF. ' + 'Please choose the converted PDF file.'; + continue; + } + return convertedData; + case _OneNoteManualXpsAction.skipFile: + return null; + case _OneNoteManualXpsAction.skipAll: + case null: + skipRemainingXps = true; + return null; + } + } + return null; + } + isPackage = isPackage || (bytes.length >= 4 && @@ -675,6 +837,7 @@ class ImportService { bytes, name: name?.trim().isNotEmpty == true ? name!.trim() : 'OneNote', isPackage: isPackage, + convertXps: convertXps, ); return _importDocument(data, document: document, advanced: advanced); } catch (e) { diff --git a/app/lib/services/onenote.dart b/app/lib/services/onenote.dart index 43f3c891e6cd..b27f25b71ad3 100644 --- a/app/lib/services/onenote.dart +++ b/app/lib/services/onenote.dart @@ -1,9 +1,12 @@ +import 'dart:collection'; +import 'dart:io'; import 'dart:math'; -import 'dart:typed_data'; import 'package:butterfly/models/defaults.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:butterfly_api/butterfly_text.dart' as text; +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:material_leap/material_leap.dart'; import 'package:onenote_parser/onenote_parser.dart' as one; @@ -14,25 +17,95 @@ const _pixelsPerHalfInch = _pixelsPerInch / 2; const _himetricUnitsPerInch = 2540.0; const _himetricToPixels = _pixelsPerInch / _himetricUnitsPerInch; +typedef OneNoteXpsConverter = + Future Function(Uint8List data, String fileName); +typedef XpsProcessRunner = + Future Function(String executable, List arguments); + +class XpsToPdfNotInstalledException implements Exception { + const XpsToPdfNotInstalledException(); + + @override + String toString() => 'xpstopdf is not installed'; +} + +Future convertXpsToPdf( + Uint8List data, { + String executable = 'xpstopdf', + XpsProcessRunner runProcess = _runProcess, +}) async { + if (kIsWeb) throw const XpsToPdfNotInstalledException(); + final directory = await Directory.systemTemp.createTemp('butterfly_xps_'); + final input = File('${directory.path}/input.xps'); + final output = File('${directory.path}/output.pdf'); + final arguments = [input.path, output.path]; + try { + await input.writeAsBytes(data); + ProcessResult result; + try { + result = await runProcess(executable, arguments); + } on ProcessException catch (error) { + if (error.errorCode == 2) { + throw const XpsToPdfNotInstalledException(); + } + rethrow; + } on UnsupportedError { + throw const XpsToPdfNotInstalledException(); + } + if (result.exitCode != 0 || !await output.exists()) { + throw ProcessException( + executable, + arguments, + result.stderr.toString(), + result.exitCode, + ); + } + return output.readAsBytes(); + } finally { + try { + await directory.delete(recursive: true); + } catch (_) {} + } +} + +Future _runProcess(String executable, List arguments) => + Process.run(executable, arguments); + Future parseOneNoteData( Uint8List bytes, { required String name, required bool isPackage, + OneNoteXpsConverter? convertXps, }) async { await (_oneNoteInitialization ??= one.RustLib.init()); if (isPackage) { final notebook = await one.parsePackageBytes(data: bytes); - return convertOneNoteNotebook(notebook, name: name); + final xpsFiles = await convertOneNoteXpsFiles( + _notebookImages(notebook), + convertXps, + ); + return convertOneNoteNotebook(notebook, name: name, xpsFiles: xpsFiles); } final section = await one.parseSectionBytes( data: bytes, fileName: '$name.one', ); - return convertOneNoteSection(section, name: name); + final xpsFiles = await convertOneNoteXpsFiles( + _sectionImages(section), + convertXps, + ); + return convertOneNoteSection(section, name: name, xpsFiles: xpsFiles); } -NoteData convertOneNoteSection(one.OneNoteSection section, {String? name}) { - final converter = _OneNoteConverter(name ?? section.displayName); +NoteData convertOneNoteSection( + one.OneNoteSection section, { + String? name, + Map? xpsFiles, +}) { + final converter = _OneNoteConverter( + name ?? section.displayName, + xpsFiles ?? HashMap.identity(), + ); converter.addSection(section, const []); return converter.finish(); } @@ -40,19 +113,151 @@ NoteData convertOneNoteSection(one.OneNoteSection section, {String? name}) { NoteData convertOneNoteNotebook( one.OneNoteNotebook notebook, { required String name, + Map? xpsFiles, }) { - final converter = _OneNoteConverter(name); + final converter = _OneNoteConverter(name, xpsFiles ?? HashMap.identity()); converter.addEntries(notebook.entries, const []); return converter.finish( warnings: notebook.warnings.map((warning) => warning.message).toList(), ); } +@visibleForTesting +Future> convertOneNoteXpsFiles( + List images, + OneNoteXpsConverter? converter, +) async { + const equality = ListEquality(); + final result = HashMap( + equals: equality.equals, + hashCode: equality.hash, + ); + final convertedPrintouts = {}; + for (final image in images) { + final data = image.data; + if (data == null || + data.isEmpty || + _fileExtension(image.extension_ ?? image.filename) != 'xps' || + result.containsKey(data)) { + continue; + } + final fileName = image.filename?.trim(); + final printoutKey = fileName == null || fileName.isEmpty + ? null + : fileName.replaceAll(r'\', '/').split('/').last.toLowerCase(); + if (printoutKey != null && convertedPrintouts.containsKey(printoutKey)) { + result[data] = convertedPrintouts[printoutKey]; + continue; + } + final converted = await (converter ?? _defaultXpsConverter)( + data, + fileName ?? 'printout.xps', + ); + result[data] = converted; + if (printoutKey != null) { + convertedPrintouts[printoutKey] = converted; + } + } + return result; +} + +Future _defaultXpsConverter(Uint8List data, String _) => + convertXpsToPdf(data); + +List _notebookImages(one.OneNoteNotebook notebook) { + final images = []; + + void addEntries(List entries) { + for (final entry in entries) { + entry.when( + section: (section) => images.addAll(_sectionImages(section)), + sectionGroup: (group) => addEntries(group.entries), + ); + } + } + + addEntries(notebook.entries); + return images; +} + +List _sectionImages(one.OneNoteSection section) { + final images = []; + + late void Function(one.OneNoteContent) addContent; + late void Function(one.OneNoteOutlineElement) addOutlineElement; + late void Function(one.OneNoteOutlineItem) addOutlineItem; + + addOutlineElement = (element) { + for (final content in element.contents) { + addContent(content); + } + for (final child in element.children) { + addOutlineItem(child); + } + }; + + addOutlineItem = (item) { + item.when( + group: (group) { + for (final item in group.items) { + addOutlineItem(item); + } + }, + element: addOutlineElement, + ); + }; + + addContent = (content) { + content.when( + richText: (_) {}, + table: (table) { + for (final row in table.rows) { + for (final cell in row.cells) { + for (final element in cell.contents) { + addOutlineElement(element); + } + } + } + }, + image: images.add, + embeddedFile: (_) {}, + ink: (_) {}, + unknown: () {}, + ); + }; + + for (final series in section.pageSeries) { + for (final page in series.pages) { + for (final content in page.contents) { + content.when( + outline: (outline) { + for (final item in outline.items) { + addOutlineItem(item); + } + }, + image: images.add, + embeddedFile: (_) {}, + ink: (_) {}, + unknown: () {}, + ); + } + } + } + return images; +} + +String _fileExtension(String? value) { + final extension = value?.split('/').last.split('.').last.toLowerCase() ?? ''; + final cleaned = extension.replaceAll(RegExp('[^a-z0-9]'), ''); + return cleaned.isEmpty ? 'bin' : cleaned; +} + class _OneNoteConverter { NoteData _document; final String name; + final Map xpsFiles; - _OneNoteConverter(this.name) + _OneNoteConverter(this.name, this.xpsFiles) : _document = DocumentDefaults.createDocument( name: name, createDefaultPage: false, @@ -75,7 +280,7 @@ class _OneNoteConverter { ].where((part) => part.trim().isNotEmpty).toList(); for (final series in section.pageSeries) { for (final page in series.pages) { - final builder = _OneNotePageBuilder(_document); + final builder = _OneNotePageBuilder(_document, xpsFiles); final converted = builder.convert(page); _document = builder.document; final title = page.title?.trim(); @@ -130,10 +335,11 @@ class _OneNoteConverter { class _OneNotePageBuilder { NoteData document; + final Map xpsFiles; final List _elements = []; final List _backgrounds = []; - _OneNotePageBuilder(this.document); + _OneNotePageBuilder(this.document, this.xpsFiles); DocumentPage convert(one.OneNotePage page) { for (final content in page.contents) { @@ -514,10 +720,6 @@ class _OneNotePageBuilder { ); } final extension = _cleanExtension(image.extension_ ?? image.filename); - String path; - final imported = document.importImage(data, extension); - document = imported.$1; - path = imported.$2; final width = (image.pictureWidth ?? image.layoutMaxWidth ?? 4) * _pixelsPerHalfInch; final height = @@ -531,6 +733,36 @@ class _OneNotePageBuilder { ? image.offsetVertical! * _pixelsPerHalfInch : fallback.y, ); + if (extension == 'xps') { + final pdf = xpsFiles[data]; + final pageNumber = image.displayedPageNumber; + if (pdf != null && pageNumber != null) { + final importedPdf = document.importPdf(pdf); + document = importedPdf.$1; + _elements.add( + PdfElement( + source: Uri.file(importedPdf.$2, windows: false).toString(), + position: position, + width: width, + height: height, + page: pageNumber, + extra: { + 'onenote:filename': ?image.filename, + 'onenote:altText': ?image.altText, + 'onenote:ocrText': ?image.ocrText, + 'onenote:displayedPageNumber': pageNumber, + }, + ), + ); + } + return image.offsetHorizontal != null || image.offsetVertical != null + ? 0 + : height; + } + + final imported = document.importImage(data, extension); + document = imported.$1; + final path = imported.$2; final source = Uri.file(path, windows: false).toString(); if (image.isBackground) { _backgrounds.add( @@ -753,10 +985,7 @@ class _OneNotePageBuilder { } String _cleanExtension(String? value) { - final extension = - value?.split('/').last.split('.').last.toLowerCase() ?? ''; - final cleaned = extension.replaceAll(RegExp('[^a-z0-9]'), ''); - return cleaned.isEmpty ? 'bin' : cleaned; + return _fileExtension(value); } double _outlineIndent( diff --git a/app/test/services/onenote_test.dart b/app/test/services/onenote_test.dart index dd9c4215d941..2118a22cfdae 100644 --- a/app/test/services/onenote_test.dart +++ b/app/test/services/onenote_test.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'dart:typed_data'; import 'package:butterfly/services/onenote.dart'; @@ -35,6 +36,80 @@ const _boldStyle = one.OneNoteTextStyle( ); void main() { + test('reports when xpstopdf is not installed', () async { + await expectLater( + convertXpsToPdf( + Uint8List.fromList([1, 2, 3]), + executable: 'butterfly-test-missing-xpstopdf', + ), + throwsA(isA()), + ); + }); + + test('passes input and output paths to xpstopdf', () async { + final pdf = Uint8List.fromList([4, 5, 6]); + late List arguments; + + final result = await convertXpsToPdf( + Uint8List.fromList([1, 2, 3]), + runProcess: (executable, passedArguments) async { + expect(executable, 'xpstopdf'); + arguments = passedArguments; + await File(passedArguments.last).writeAsBytes(pdf); + return ProcessResult(1, 0, '', ''); + }, + ); + + expect(arguments, hasLength(2)); + expect(arguments.first, endsWith('input.xps')); + expect(arguments.last, endsWith('output.pdf')); + expect(result, pdf); + }); + + test('reports non-zero xpstopdf exits as conversion failures', () async { + await expectLater( + convertXpsToPdf( + Uint8List.fromList([1, 2, 3]), + runProcess: (_, _) async => ProcessResult(1, 1, '', 'invalid XPS'), + ), + throwsA(isA()), + ); + }); + + test('converts all pages of the same XPS printout only once', () async { + final firstPageData = Uint8List.fromList([1, 2, 3]); + final secondPageData = Uint8List.fromList([4, 5, 6]); + final pdf = Uint8List.fromList([7, 8, 9]); + var conversions = 0; + + final converted = await convertOneNoteXpsFiles( + [ + one.OneNoteImage( + data: firstPageData, + extension_: 'xps', + filename: r'Printouts\document.xps', + displayedPageNumber: 1, + isBackground: false, + ), + one.OneNoteImage( + data: secondPageData, + extension_: 'xps', + filename: 'document.xps', + displayedPageNumber: 2, + isBackground: false, + ), + ], + (data, fileName) async { + conversions++; + return pdf; + }, + ); + + expect(conversions, 1); + expect(converted[firstPageData], pdf); + expect(converted[secondPageData], pdf); + }); + test('converts OneNote page content and embedded assets', () { final richText = one.OneNoteRichText( text: 'Hello world', @@ -223,6 +298,124 @@ void main() { expect(imported.property.strokeWidth, closeTo(3.7795, 0.0001)); }); + test('uses converted PDF data for XPS printouts', () { + final xps = Uint8List.fromList([1, 2, 3]); + final pdf = Uint8List.fromList([4, 5, 6]); + final page = one.OneNotePage( + linkTargetId: 'printout-page', + title: 'Printout', + level: 0, + createdAt: '', + updatedAt: '', + contents: [ + one.OneNotePageContent.image( + one.OneNoteImage( + data: xps, + extension_: 'xps', + filename: 'printout.xps', + displayedPageNumber: 2, + pictureWidth: 4, + pictureHeight: 3, + isBackground: false, + ), + ), + ], + ); + final section = one.OneNoteSection( + displayName: 'Files', + pageSeries: [ + one.OneNotePageSeries(pages: [page]), + ], + warnings: const [], + ); + + final document = convertOneNoteSection(section, xpsFiles: {xps: pdf}); + final convertedPage = document.getPage('Files/Printout')!; + final printout = convertedPage.content.whereType().single; + + expect(printout.page, 1); + expect(printout.width, 192); + expect(printout.height, 144); + expect(document.getAsset(Uri.parse(printout.source).path), pdf); + expect(convertedPage.content.whereType(), isEmpty); + }); + + test('creates one PDF element per XPS page without image duplicates', () { + final firstXps = Uint8List.fromList([1, 2, 3]); + final secondXps = Uint8List.fromList([4, 5, 6]); + final pdf = Uint8List.fromList([7, 8, 9]); + final page = one.OneNotePage( + linkTargetId: 'printout-page', + title: 'Printout', + level: 0, + createdAt: '', + updatedAt: '', + contents: [ + for (final (index, data) in [firstXps, secondXps].indexed) + one.OneNotePageContent.image( + one.OneNoteImage( + data: data, + extension_: 'xps', + filename: 'document.xps', + displayedPageNumber: index + 1, + isBackground: false, + ), + ), + ], + ); + final section = one.OneNoteSection( + displayName: 'Files', + pageSeries: [ + one.OneNotePageSeries(pages: [page]), + ], + warnings: const [], + ); + + final document = convertOneNoteSection( + section, + xpsFiles: {firstXps: pdf, secondXps: pdf}, + ); + final content = document.getPage('Files/Printout')!.content; + + expect(content.whereType().map((element) => element.page), [ + 0, + 1, + ]); + expect(content.whereType(), isEmpty); + }); + + test('skips XPS printouts without converted PDF data', () { + final xps = Uint8List.fromList([1, 2, 3]); + final page = one.OneNotePage( + linkTargetId: 'printout-page', + title: 'Printout', + level: 0, + createdAt: '', + updatedAt: '', + contents: [ + one.OneNotePageContent.image( + one.OneNoteImage( + data: xps, + extension_: 'xps', + displayedPageNumber: 1, + isBackground: false, + ), + ), + ], + ); + final section = one.OneNoteSection( + displayName: 'Files', + pageSeries: [ + one.OneNotePageSeries(pages: [page]), + ], + warnings: const [], + ); + + final document = convertOneNoteSection(section, xpsFiles: {xps: null}); + + expect(document.getPage('Files/Printout')!.content, isEmpty); + }); + test('positions embedded ink relative to its bounding box', () { const ink = one.OneNoteInk( strokes: [ From 66307721c14728b9a299da95cb77ecf380015954 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 21 Jun 2026 22:34:09 +0200 Subject: [PATCH 005/117] Add web support to onenote importer --- .github/workflows/deploy.yml | 7 ++ app/.gitignore | 1 + app/lib/services/import.dart | 25 ++-- app/lib/services/onenote.dart | 25 +++- app/lib/services/onenote_library.dart | 2 + app/lib/services/onenote_library_io.dart | 1 + app/lib/services/onenote_library_web.dart | 41 +++++++ app/test/services/onenote_test.dart | 15 ++- tools/build_onenote_web.dart | 133 ++++++++++++++++++++++ 9 files changed, 239 insertions(+), 11 deletions(-) create mode 100644 app/lib/services/onenote_library.dart create mode 100644 app/lib/services/onenote_library_io.dart create mode 100644 app/lib/services/onenote_library_web.dart create mode 100644 tools/build_onenote_web.dart diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index dea2dd039ebc..42c6c4f816c4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -57,6 +57,13 @@ jobs: - name: Install dependencies run: | flutter pub get + - name: Install OneNote web toolchain + run: | + rustup toolchain install nightly --component rust-src + cargo install wasm-pack --locked + - name: Build OneNote web library + working-directory: . + run: dart run tools/build_onenote_web.dart - name: Get Git Commit Hash run: echo "GIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV - name: Set flavor diff --git a/app/.gitignore b/app/.gitignore index 5135d75da2e2..721abd9cf6c6 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -34,6 +34,7 @@ /lib/src/generated # Web related +/web/pkg/ # Symbolication related app.*.symbols diff --git a/app/lib/services/import.dart b/app/lib/services/import.dart index 1e393bb713ac..f8c766e2e074 100644 --- a/app/lib/services/import.dart +++ b/app/lib/services/import.dart @@ -806,11 +806,18 @@ class ImportService { message = 'No PDF was selected.'; continue; } - final convertedData = await converted.readAsBytes(); - if (convertedData.length < 5 || - String.fromCharCodes(convertedData.take(5)) != '%PDF-') { + Uint8List convertedData; + try { + convertedData = await converted.readAsBytes(); + } catch (error) { + message = + 'Butterfly could not read “${converted.name}”: $error'; + continue; + } + if (!isPdfData(convertedData)) { message = 'The selected file does not appear to be a valid PDF. ' + 'A PDF header was not found in the first 1024 bytes. ' 'Please choose the converted PDF file.'; continue; } @@ -841,11 +848,13 @@ class ImportService { ); return _importDocument(data, document: document, advanced: advanced); } catch (e) { - showDialog( - context: context, - builder: (context) => - UnknownImportConfirmationDialog(message: e.toString()), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (context) => + UnknownImportConfirmationDialog(message: e.toString()), + ); + } } return null; } diff --git a/app/lib/services/onenote.dart b/app/lib/services/onenote.dart index b27f25b71ad3..9becf575eda4 100644 --- a/app/lib/services/onenote.dart +++ b/app/lib/services/onenote.dart @@ -10,6 +10,8 @@ import 'package:flutter/foundation.dart'; import 'package:material_leap/material_leap.dart'; import 'package:onenote_parser/onenote_parser.dart' as one; +import 'onenote_library.dart'; + Future? _oneNoteInitialization; const _pixelsPerInch = 96.0; @@ -71,13 +73,29 @@ Future convertXpsToPdf( Future _runProcess(String executable, List arguments) => Process.run(executable, arguments); +bool isPdfData(Uint8List data) { + const header = [0x25, 0x50, 0x44, 0x46, 0x2D]; // %PDF- + final searchLength = min(data.length, 1024); + for (var offset = 0; offset <= searchLength - header.length; offset++) { + var matches = true; + for (var index = 0; index < header.length; index++) { + if (data[offset + index] != header[index]) { + matches = false; + break; + } + } + if (matches) return true; + } + return false; +} + Future parseOneNoteData( Uint8List bytes, { required String name, required bool isPackage, OneNoteXpsConverter? convertXps, }) async { - await (_oneNoteInitialization ??= one.RustLib.init()); + await (_oneNoteInitialization ??= _initializeOneNoteParser()); if (isPackage) { final notebook = await one.parsePackageBytes(data: bytes); final xpsFiles = await convertOneNoteXpsFiles( @@ -97,6 +115,11 @@ Future parseOneNoteData( return convertOneNoteSection(section, name: name, xpsFiles: xpsFiles); } +Future _initializeOneNoteParser() async { + await ensureOneNoteLibraryAvailable(); + await one.RustLib.init(); +} + NoteData convertOneNoteSection( one.OneNoteSection section, { String? name, diff --git a/app/lib/services/onenote_library.dart b/app/lib/services/onenote_library.dart new file mode 100644 index 000000000000..de75e601b9ab --- /dev/null +++ b/app/lib/services/onenote_library.dart @@ -0,0 +1,2 @@ +export 'onenote_library_io.dart' + if (dart.library.js_interop) 'onenote_library_web.dart'; diff --git a/app/lib/services/onenote_library_io.dart b/app/lib/services/onenote_library_io.dart new file mode 100644 index 000000000000..d44c29353a83 --- /dev/null +++ b/app/lib/services/onenote_library_io.dart @@ -0,0 +1 @@ +Future ensureOneNoteLibraryAvailable() async {} diff --git a/app/lib/services/onenote_library_web.dart b/app/lib/services/onenote_library_web.dart new file mode 100644 index 000000000000..328c6252ecc1 --- /dev/null +++ b/app/lib/services/onenote_library_web.dart @@ -0,0 +1,41 @@ +import 'package:http/http.dart' as http; + +const _oneNoteLibraryFiles = [ + ('pkg/rust_lib_onenote_parser.js', 'JavaScript'), + ('pkg/rust_lib_onenote_parser_bg.wasm', 'WebAssembly'), +]; + +Future ensureOneNoteLibraryAvailable() async { + for (final (path, label) in _oneNoteLibraryFiles) { + late http.Response response; + try { + response = await http.head(Uri.base.resolve(path)); + } catch (error) { + throw OneNoteLibraryNotFoundException( + '$label library request failed for “$path”: $error', + ); + } + final contentType = response.headers['content-type']?.toLowerCase() ?? ''; + if (response.statusCode < 200 || + response.statusCode >= 300 || + contentType.contains('text/html')) { + throw OneNoteLibraryNotFoundException( + '$label library “$path” was not found ' + '(HTTP ${response.statusCode}, content type ' + '“${contentType.isEmpty ? 'unknown' : contentType}”).', + ); + } + } +} + +class OneNoteLibraryNotFoundException implements Exception { + const OneNoteLibraryNotFoundException(this.details); + + final String details; + + @override + String toString() => + 'The OneNote parser web library is missing. $details\n\n' + 'Run "dart run tools/build_onenote_web.dart" before starting or ' + 'building Butterfly for web.'; +} diff --git a/app/test/services/onenote_test.dart b/app/test/services/onenote_test.dart index 2118a22cfdae..4d41c3d4d1fb 100644 --- a/app/test/services/onenote_test.dart +++ b/app/test/services/onenote_test.dart @@ -76,6 +76,17 @@ void main() { ); }); + test('accepts PDF headers within the first 1024 bytes', () { + expect(isPdfData(Uint8List.fromList('%PDF-1.7'.codeUnits)), isTrue); + expect( + isPdfData( + Uint8List.fromList([...List.filled(32, 0), ...'%PDF-1.7'.codeUnits]), + ), + isTrue, + ); + expect(isPdfData(Uint8List.fromList('not a pdf'.codeUnits)), isFalse); + }); + test('converts all pages of the same XPS printout only once', () async { final firstPageData = Uint8List.fromList([1, 2, 3]); final secondPageData = Uint8List.fromList([4, 5, 6]); @@ -333,7 +344,7 @@ void main() { final convertedPage = document.getPage('Files/Printout')!; final printout = convertedPage.content.whereType().single; - expect(printout.page, 1); + expect(printout.page, 2); expect(printout.width, 192); expect(printout.height, 144); expect(document.getAsset(Uri.parse(printout.source).path), pdf); @@ -378,8 +389,8 @@ void main() { final content = document.getPage('Files/Printout')!.content; expect(content.whereType().map((element) => element.page), [ - 0, 1, + 2, ]); expect(content.whereType(), isEmpty); }); diff --git a/tools/build_onenote_web.dart b/tools/build_onenote_web.dart new file mode 100644 index 000000000000..f10b12d06a9a --- /dev/null +++ b/tools/build_onenote_web.dart @@ -0,0 +1,133 @@ +import 'dart:convert'; +import 'dart:io'; + +Future main() async { + final repositoryRoot = _findRepositoryRoot(); + final appDirectory = Directory('${repositoryRoot.path}/app'); + final packageConfig = File( + '${appDirectory.path}/.dart_tool/package_config.json', + ); + if (!packageConfig.existsSync()) { + _fail( + 'Missing ${packageConfig.path}.\n' + 'Run "flutter pub get" in ${appDirectory.path} first.', + ); + } + + final config = + jsonDecode(await packageConfig.readAsString()) as Map; + final packages = config['packages'] as List; + final parserEntries = packages.cast>().where( + (entry) => entry['name'] == 'onenote_parser', + ); + if (parserEntries.isEmpty) { + _fail( + 'The onenote_parser package is not present in package_config.json.\n' + 'Run "flutter pub get" in ${appDirectory.path} first.', + ); + } + + final packageRoot = packageConfig.uri + .resolve(parserEntries.single['rootUri'] as String) + .toFilePath(); + final rustRoot = Directory('$packageRoot/rust'); + if (!File('${rustRoot.path}/Cargo.toml').existsSync()) { + _fail('Could not find the onenote_parser Rust crate at ${rustRoot.path}.'); + } + + final rustup = await Process.run('rustup', [ + 'component', + 'list', + '--toolchain', + 'nightly', + '--installed', + ]); + if (rustup.exitCode != 0 || + !rustup.stdout.toString().split('\n').contains('rust-src')) { + _fail( + 'The Rust nightly rust-src component is required.\n' + 'Install it with:\n' + ' rustup toolchain install nightly --component rust-src', + ); + } + + if (!await _commandExists('wasm-pack')) { + _fail( + 'wasm-pack is required to build the OneNote parser for web.\n' + 'Install it with:\n' + ' cargo install wasm-pack --locked', + ); + } + + final outputDirectory = Directory('${appDirectory.path}/web'); + final generatedDirectory = Directory('${outputDirectory.path}/pkg'); + if (generatedDirectory.existsSync()) { + await generatedDirectory.delete(recursive: true); + } + + stdout.writeln('Building the OneNote parser web library...'); + final process = await Process.start( + 'dart', + [ + 'run', + 'flutter_rust_bridge:flutter_rust_bridge', + 'build-web', + '--dart-root', + packageRoot, + '--rust-root', + rustRoot.path, + '--output', + outputDirectory.path, + '--release', + ], + workingDirectory: appDirectory.path, + mode: ProcessStartMode.inheritStdio, + ); + final exitCode = await process.exitCode; + if (exitCode != 0) { + _fail('Failed to build the OneNote parser web library.', exitCode); + } + + final expectedFiles = [ + File('${generatedDirectory.path}/rust_lib_onenote_parser.js'), + File('${generatedDirectory.path}/rust_lib_onenote_parser_bg.wasm'), + ]; + final missingFiles = expectedFiles.where((file) => !file.existsSync()); + if (missingFiles.isNotEmpty) { + _fail( + 'The OneNote parser build completed without producing:\n' + '${missingFiles.map((file) => ' ${file.path}').join('\n')}', + ); + } + + stdout.writeln('OneNote parser web library written to app/web/pkg.'); +} + +Directory _findRepositoryRoot() { + var directory = Directory.current.absolute; + while (true) { + if (File('${directory.path}/app/pubspec.yaml').existsSync() && + File('${directory.path}/tools/pubspec.yaml').existsSync()) { + return directory; + } + final parent = directory.parent; + if (parent.path == directory.path) { + _fail( + 'Could not find the Butterfly repository root from ' + '${Directory.current.path}.', + ); + } + directory = parent; + } +} + +Future _commandExists(String command) async { + final finder = Platform.isWindows ? 'where.exe' : 'which'; + final result = await Process.run(finder, [command]); + return result.exitCode == 0; +} + +Never _fail(String message, [int exitCode = 1]) { + stderr.writeln(message); + exit(exitCode); +} From 6e0126cc8585bb610451539a6c5ac03d0ee11b28 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 21 Jun 2026 22:42:15 +0200 Subject: [PATCH 006/117] Fix snap build for onenote parser --- app/snapcraft.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/snapcraft.yaml b/app/snapcraft.yaml index 1ab69f0d8de3..94ced03ebe82 100644 --- a/app/snapcraft.yaml +++ b/app/snapcraft.yaml @@ -63,6 +63,7 @@ parts: - libjsoncpp-dev - clang - ninja-build + - rustup - pkg-config - lld - llvm From 6aa8300197d5a0695dfb404113d1cc9180e17bdc Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 21 Jun 2026 22:58:47 +0200 Subject: [PATCH 007/117] Add general file import, add onenote type, add onenote loading --- api/lib/src/models/tool.dart | 15 ++++++++-- api/lib/src/models/tool.freezed.dart | 2 +- api/lib/src/models/tool.g.dart | 4 ++- app/lib/dialogs/import/add.dart | 6 +++- app/lib/dialogs/load.dart | 2 +- app/lib/handlers/asset.dart | 7 +++++ app/lib/selections/selection.dart | 1 + app/lib/selections/tools/asset.dart | 44 ++++++++++++++++++++++++++++ app/lib/selections/tools/tool.dart | 1 + app/lib/services/import.dart | 30 ++++++++++++++++++- app/lib/visualizer/tool.dart | 4 +++ 11 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 app/lib/selections/tools/asset.dart diff --git a/api/lib/src/models/tool.dart b/api/lib/src/models/tool.dart index b443094bce88..3e8c8f092e82 100644 --- a/api/lib/src/models/tool.dart +++ b/api/lib/src/models/tool.dart @@ -22,7 +22,18 @@ enum LabelMode { markdown, text } enum Axis2D { horizontal, vertical } -enum ImportType { image, camera, svg, svgText, pdf, document, markdown, xopp } +enum ImportType { + file, + oneNote, + image, + camera, + svg, + svgText, + pdf, + document, + markdown, + xopp, +} enum SelectMode { rectangle, lasso } @@ -207,7 +218,7 @@ sealed class Tool extends PackAsset with _$Tool { @Default('') String name, @Default('') String displayIcon, @IdJsonConverter() String? id, - @Default(ImportType.document) ImportType importType, + @Default(ImportType.file) ImportType importType, @Default(true) bool advanced, }) = AssetTool; diff --git a/api/lib/src/models/tool.freezed.dart b/api/lib/src/models/tool.freezed.dart index 7259be5eea6d..4814bdfe7305 100644 --- a/api/lib/src/models/tool.freezed.dart +++ b/api/lib/src/models/tool.freezed.dart @@ -1422,7 +1422,7 @@ as String?, @JsonSerializable() class AssetTool extends Tool { - AssetTool({this.name = '', this.displayIcon = '', @IdJsonConverter() this.id, this.importType = ImportType.document, this.advanced = true, final String? $type}): $type = $type ?? 'asset',super._(); + AssetTool({this.name = '', this.displayIcon = '', @IdJsonConverter() this.id, this.importType = ImportType.file, this.advanced = true, final String? $type}): $type = $type ?? 'asset',super._(); factory AssetTool.fromJson(Map json) => _$AssetToolFromJson(json); @override@JsonKey() final String name; diff --git a/api/lib/src/models/tool.g.dart b/api/lib/src/models/tool.g.dart index 016f674d6eaa..c5bb540fb3d0 100644 --- a/api/lib/src/models/tool.g.dart +++ b/api/lib/src/models/tool.g.dart @@ -401,7 +401,7 @@ AssetTool _$AssetToolFromJson(Map json) => AssetTool( id: const IdJsonConverter().fromJson(json['id'] as String?), importType: $enumDecodeNullable(_$ImportTypeEnumMap, json['importType']) ?? - ImportType.document, + ImportType.file, advanced: json['advanced'] as bool? ?? true, $type: json['type'] as String?, ); @@ -416,6 +416,8 @@ Map _$AssetToolToJson(AssetTool instance) => { }; const _$ImportTypeEnumMap = { + ImportType.file: 'file', + ImportType.oneNote: 'oneNote', ImportType.image: 'image', ImportType.camera: 'camera', ImportType.svg: 'svg', diff --git a/app/lib/dialogs/import/add.dart b/app/lib/dialogs/import/add.dart index a2fb6792c4c7..2c7c530f73ed 100644 --- a/app/lib/dialogs/import/add.dart +++ b/app/lib/dialogs/import/add.dart @@ -90,7 +90,11 @@ class _AddDialogState extends State { Future> _getAvailableImports() async { final imports = await Future.wait( - ImportType.values.map((type) async => (type, await type.isAvailable())), + const [ + ImportType.file, + ImportType.oneNote, + ImportType.camera, + ].map((type) async => (type, await type.isAvailable())), ); return imports diff --git a/app/lib/dialogs/load.dart b/app/lib/dialogs/load.dart index 04444e795d5f..a7c926054473 100644 --- a/app/lib/dialogs/load.dart +++ b/app/lib/dialogs/load.dart @@ -47,7 +47,7 @@ class LoadingDialog extends StatefulWidget { } class _LoadingDialogState extends State { - double _progress = 0.0; + double? _progress; void setProgress(double progress) => setState(() { _progress = progress; diff --git a/app/lib/handlers/asset.dart b/app/lib/handlers/asset.dart index ad614fb696be..3900c9cf9def 100644 --- a/app/lib/handlers/asset.dart +++ b/app/lib/handlers/asset.dart @@ -60,6 +60,13 @@ Future showImportAssetWizard( if (!await type.isAvailable()) return; switch (type) { + case ImportType.file: + return importWithDialog(AssetFileType.values); + case ImportType.oneNote: + return importWithDialog([ + AssetFileType.oneNote, + AssetFileType.oneNotePackage, + ]); case ImportType.image: return importWithDialog([AssetFileType.image]); case ImportType.camera: diff --git a/app/lib/selections/selection.dart b/app/lib/selections/selection.dart index e351e7ff89b2..3efa66fc3d94 100644 --- a/app/lib/selections/selection.dart +++ b/app/lib/selections/selection.dart @@ -35,6 +35,7 @@ part 'elements/shape.dart'; part 'elements/svg.dart'; part 'tools/barcode.dart'; +part 'tools/asset.dart'; part 'tools/tool.dart'; part 'tools/hand.dart'; part 'tools/area.dart'; diff --git a/app/lib/selections/tools/asset.dart b/app/lib/selections/tools/asset.dart new file mode 100644 index 000000000000..48fe1428af18 --- /dev/null +++ b/app/lib/selections/tools/asset.dart @@ -0,0 +1,44 @@ +part of '../selection.dart'; + +class AssetToolSelection extends ToolSelection { + AssetToolSelection(super.selected); + + @override + List buildProperties(BuildContext context) { + return [ + ...super.buildProperties(context), + ListTile( + title: Text(AppLocalizations.of(context).type), + trailing: DropdownMenu( + initialSelection: selected.first.importType, + dropdownMenuEntries: ImportType.values + .map( + (type) => DropdownMenuEntry( + value: type, + label: type.getLocalizedName(context), + leadingIcon: PhosphorIcon( + type.icon(PhosphorIconsStyle.light), + ), + ), + ) + .toList(), + onSelected: (type) { + if (type == null) return; + update( + context, + selected.map((tool) => tool.copyWith(importType: type)).toList(), + ); + }, + ), + ), + ]; + } + + @override + Selection insert(dynamic element) { + if (element is AssetTool) { + return AssetToolSelection([...selected, element]); + } + return super.insert(element); + } +} diff --git a/app/lib/selections/tools/tool.dart b/app/lib/selections/tools/tool.dart index dbe4fd40ba4e..25c522d44e07 100644 --- a/app/lib/selections/tools/tool.dart +++ b/app/lib/selections/tools/tool.dart @@ -18,6 +18,7 @@ class ToolSelection extends Selection { StampTool e => StampToolSelection([e]), TextureTool e => TextureToolSelection([e]), BarcodeTool e => BarcodeToolSelection([e]), + AssetTool e => AssetToolSelection([e]), PolygonTool e => PolygonToolSelection([e]), SpacerTool e => SpacerToolSelection([e]), _ => ToolSelection([selected]), diff --git a/app/lib/services/import.dart b/app/lib/services/import.dart index f8c766e2e074..60b922856d75 100644 --- a/app/lib/services/import.dart +++ b/app/lib/services/import.dart @@ -668,7 +668,21 @@ class ImportService { bool advanced = true, String? name, }) async { + LoadingDialogHandler? loadingDialog; + + Future showLoading() async { + if (!context.mounted || loadingDialog != null) return; + loadingDialog = showLoadingDialog(context); + await Future.delayed(Duration.zero); + } + + void hideLoading() { + loadingDialog?.close(); + loadingDialog = null; + } + try { + await showLoading(); var manuallyConvertXps = false; var skipRemainingXps = false; @@ -684,6 +698,7 @@ class ImportService { if (!context.mounted) return null; final executableMissing = conversionError is XpsToPdfNotInstalledException; + hideLoading(); final fallback = await showDialog<_OneNoteXpsFallback>( context: context, builder: (context) => AlertDialog( @@ -720,6 +735,7 @@ class ImportService { ], ), ); + await showLoading(); manuallyConvertXps = fallback == _OneNoteXpsFallback.manual; if (!manuallyConvertXps) { skipRemainingXps = true; @@ -734,6 +750,7 @@ class ImportService { var exported = false; String? message; while (context.mounted) { + hideLoading(); final action = await showDialog<_OneNoteManualXpsAction>( context: context, barrierDismissible: false, @@ -821,12 +838,15 @@ class ImportService { 'Please choose the converted PDF file.'; continue; } + await showLoading(); return convertedData; case _OneNoteManualXpsAction.skipFile: + await showLoading(); return null; case _OneNoteManualXpsAction.skipAll: case null: skipRemainingXps = true; + await showLoading(); return null; } } @@ -846,8 +866,14 @@ class ImportService { isPackage: isPackage, convertXps: convertXps, ); - return _importDocument(data, document: document, advanced: advanced); + hideLoading(); + return await _importDocument( + data, + document: document, + advanced: advanced, + ); } catch (e) { + hideLoading(); if (context.mounted) { await showDialog( context: context, @@ -855,6 +881,8 @@ class ImportService { UnknownImportConfirmationDialog(message: e.toString()), ); } + } finally { + hideLoading(); } return null; } diff --git a/app/lib/visualizer/tool.dart b/app/lib/visualizer/tool.dart index 28d37200f8aa..668a2d59027b 100644 --- a/app/lib/visualizer/tool.dart +++ b/app/lib/visualizer/tool.dart @@ -219,6 +219,8 @@ extension ToolVisualizer on Tool { extension ImportTypeVisualizer on ImportType { String getLocalizedName(BuildContext context) => switch (this) { + ImportType.file => AppLocalizations.of(context).import, + ImportType.oneNote => 'OneNote', ImportType.document => AppLocalizations.of(context).document, ImportType.image => AppLocalizations.of(context).image, ImportType.pdf => AppLocalizations.of(context).pdf, @@ -230,6 +232,8 @@ extension ImportTypeVisualizer on ImportType { }; IconGetter get icon => switch (this) { + ImportType.file => PhosphorIcons.fileArrowUp, + ImportType.oneNote => PhosphorIcons.notebook, ImportType.document => PhosphorIcons.fileText, ImportType.image => PhosphorIcons.image, ImportType.pdf => PhosphorIcons.filePdf, From e0781fbcacb37fd43f714af1d12dc389deb147b4 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 23 Jun 2026 13:37:43 +0200 Subject: [PATCH 008/117] Add combine highlighter option, closes #1071 --- api/lib/src/models/element.dart | 1 + api/lib/src/models/element.freezed.dart | 16 +- api/lib/src/models/element.g.dart | 2 + api/lib/src/models/tool.dart | 1 + api/lib/src/models/tool.freezed.dart | 10 +- api/lib/src/models/tool.g.dart | 2 + api/test/data_test.dart | 26 +++ app/lib/cubits/current_index.dart | 9 + app/lib/handlers/pen.dart | 1 + app/lib/l10n/app_en.arb | 1 + app/lib/renderers/elements/pen.dart | 52 ++--- app/lib/selections/tools/pen.dart | 10 + app/lib/view_painter.dart | 181 ++++++++++++------ app/test/bloc/document_bloc_test.dart | 69 +++++++ .../renderers/highlighter_renderer_test.dart | 80 ++++++++ 15 files changed, 367 insertions(+), 94 deletions(-) create mode 100644 app/test/renderers/highlighter_renderer_test.dart diff --git a/api/lib/src/models/element.dart b/api/lib/src/models/element.dart index e0260a27101a..2185a1cf67c0 100644 --- a/api/lib/src/models/element.dart +++ b/api/lib/src/models/element.dart @@ -88,6 +88,7 @@ sealed class PadElement with _$PadElement { @Default('') String collection, @IdJsonConverter() String? id, double? zoom, + String? combineId, @Default([]) List points, @Default(PenProperty()) PenProperty property, @Default({}) Map extra, diff --git a/api/lib/src/models/element.freezed.dart b/api/lib/src/models/element.freezed.dart index 0867107ec9d6..7ccc20bda3d9 100644 --- a/api/lib/src/models/element.freezed.dart +++ b/api/lib/src/models/element.freezed.dart @@ -564,13 +564,14 @@ as Map, @JsonSerializable() class PenElement extends PadElement implements PathElement { - PenElement({this.rotation = 0, this.collection = '', @IdJsonConverter() this.id, this.zoom, final List points = const [], this.property = const PenProperty(), final Map extra = const {}, final String? $type}): _points = points,_extra = extra,$type = $type ?? 'pen',super._(); + PenElement({this.rotation = 0, this.collection = '', @IdJsonConverter() this.id, this.zoom, this.combineId, final List points = const [], this.property = const PenProperty(), final Map extra = const {}, final String? $type}): _points = points,_extra = extra,$type = $type ?? 'pen',super._(); factory PenElement.fromJson(Map json) => _$PenElementFromJson(json); @override@JsonKey() final double rotation; @override@JsonKey() final String collection; @override@IdJsonConverter() final String? id; final double? zoom; + final String? combineId; final List _points; @JsonKey() List get points { if (_points is EqualUnmodifiableListView) return _points; @@ -604,16 +605,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is PenElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&(identical(other.zoom, zoom) || other.zoom == zoom)&&const DeepCollectionEquality().equals(other._points, _points)&&const DeepCollectionEquality().equals(other.property, property)&&const DeepCollectionEquality().equals(other._extra, _extra)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is PenElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&(identical(other.zoom, zoom) || other.zoom == zoom)&&(identical(other.combineId, combineId) || other.combineId == combineId)&&const DeepCollectionEquality().equals(other._points, _points)&&const DeepCollectionEquality().equals(other.property, property)&&const DeepCollectionEquality().equals(other._extra, _extra)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,rotation,collection,id,zoom,const DeepCollectionEquality().hash(_points),const DeepCollectionEquality().hash(property),const DeepCollectionEquality().hash(_extra)); +int get hashCode => Object.hash(runtimeType,rotation,collection,id,zoom,combineId,const DeepCollectionEquality().hash(_points),const DeepCollectionEquality().hash(property),const DeepCollectionEquality().hash(_extra)); @override String toString() { - return 'PadElement.pen(rotation: $rotation, collection: $collection, id: $id, zoom: $zoom, points: $points, property: $property, extra: $extra)'; + return 'PadElement.pen(rotation: $rotation, collection: $collection, id: $id, zoom: $zoom, combineId: $combineId, points: $points, property: $property, extra: $extra)'; } @@ -624,7 +625,7 @@ abstract mixin class $PenElementCopyWith<$Res> implements $PadElementCopyWith<$R factory $PenElementCopyWith(PenElement value, $Res Function(PenElement) _then) = _$PenElementCopyWithImpl; @override @useResult $Res call({ - double rotation, String collection,@IdJsonConverter() String? id, double? zoom, List points, PenProperty property, Map extra + double rotation, String collection,@IdJsonConverter() String? id, double? zoom, String? combineId, List points, PenProperty property, Map extra }); @@ -641,13 +642,14 @@ class _$PenElementCopyWithImpl<$Res> /// Create a copy of PadElement /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? collection = null,Object? id = freezed,Object? zoom = freezed,Object? points = null,Object? property = freezed,Object? extra = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? collection = null,Object? id = freezed,Object? zoom = freezed,Object? combineId = freezed,Object? points = null,Object? property = freezed,Object? extra = null,}) { return _then(PenElement( rotation: null == rotation ? _self.rotation : rotation // ignore: cast_nullable_to_non_nullable as double,collection: null == collection ? _self.collection : collection // ignore: cast_nullable_to_non_nullable as String,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String?,zoom: freezed == zoom ? _self.zoom : zoom // ignore: cast_nullable_to_non_nullable -as double?,points: null == points ? _self._points : points // ignore: cast_nullable_to_non_nullable +as double?,combineId: freezed == combineId ? _self.combineId : combineId // ignore: cast_nullable_to_non_nullable +as String?,points: null == points ? _self._points : points // ignore: cast_nullable_to_non_nullable as List,property: freezed == property ? _self.property : property // ignore: cast_nullable_to_non_nullable as PenProperty,extra: null == extra ? _self._extra : extra // ignore: cast_nullable_to_non_nullable as Map, diff --git a/api/lib/src/models/element.g.dart b/api/lib/src/models/element.g.dart index 386b9668a61b..b35bb2080330 100644 --- a/api/lib/src/models/element.g.dart +++ b/api/lib/src/models/element.g.dart @@ -73,6 +73,7 @@ PenElement _$PenElementFromJson(Map json) => PenElement( collection: json['collection'] as String? ?? '', id: const IdJsonConverter().fromJson(json['id'] as String?), zoom: (json['zoom'] as num?)?.toDouble(), + combineId: json['combineId'] as String?, points: (json['points'] as List?) ?.map((e) => PathPoint.fromJson(Map.from(e as Map))) @@ -95,6 +96,7 @@ Map _$PenElementToJson(PenElement instance) => 'collection': instance.collection, 'id': const IdJsonConverter().toJson(instance.id), 'zoom': instance.zoom, + 'combineId': instance.combineId, 'points': instance.points.map((e) => e.toJson()).toList(), 'property': instance.property.toJson(), 'extra': instance.extra, diff --git a/api/lib/src/models/tool.dart b/api/lib/src/models/tool.dart index 3e8c8f092e82..b217d82028a0 100644 --- a/api/lib/src/models/tool.dart +++ b/api/lib/src/models/tool.dart @@ -133,6 +133,7 @@ sealed class Tool extends PackAsset with _$Tool { @Default(false) bool zoomDependent, @Default(0.5) double shapeDetectionTime, @Default(false) bool shapeDetectionEnabled, + @Default(false) bool combineHighlights, @Default(PenProperty()) PenProperty property, }) = PenTool; diff --git a/api/lib/src/models/tool.freezed.dart b/api/lib/src/models/tool.freezed.dart index 4814bdfe7305..8954c5fe56a6 100644 --- a/api/lib/src/models/tool.freezed.dart +++ b/api/lib/src/models/tool.freezed.dart @@ -656,7 +656,7 @@ $NamedItemCopyWith? get styleSheet { @JsonSerializable() class PenTool extends Tool { - PenTool({this.name = '', this.displayIcon = '', @IdJsonConverter() this.id, this.zoomDependent = false, this.shapeDetectionTime = 0.5, this.shapeDetectionEnabled = false, this.property = const PenProperty(), final String? $type}): $type = $type ?? 'pen',super._(); + PenTool({this.name = '', this.displayIcon = '', @IdJsonConverter() this.id, this.zoomDependent = false, this.shapeDetectionTime = 0.5, this.shapeDetectionEnabled = false, this.combineHighlights = false, this.property = const PenProperty(), final String? $type}): $type = $type ?? 'pen',super._(); factory PenTool.fromJson(Map json) => _$PenToolFromJson(json); @override@JsonKey() final String name; @@ -665,6 +665,7 @@ class PenTool extends Tool { @JsonKey() final bool zoomDependent; @JsonKey() final double shapeDetectionTime; @JsonKey() final bool shapeDetectionEnabled; +@JsonKey() final bool combineHighlights; @JsonKey() final PenProperty property; @JsonKey(name: 'type') @@ -686,7 +687,7 @@ Map toJson() { @override String toString() { - return 'Tool.pen(name: $name, displayIcon: $displayIcon, id: $id, zoomDependent: $zoomDependent, shapeDetectionTime: $shapeDetectionTime, shapeDetectionEnabled: $shapeDetectionEnabled, property: $property)'; + return 'Tool.pen(name: $name, displayIcon: $displayIcon, id: $id, zoomDependent: $zoomDependent, shapeDetectionTime: $shapeDetectionTime, shapeDetectionEnabled: $shapeDetectionEnabled, combineHighlights: $combineHighlights, property: $property)'; } @@ -697,7 +698,7 @@ abstract mixin class $PenToolCopyWith<$Res> implements $ToolCopyWith<$Res> { factory $PenToolCopyWith(PenTool value, $Res Function(PenTool) _then) = _$PenToolCopyWithImpl; @override @useResult $Res call({ - String name, String displayIcon,@IdJsonConverter() String? id, bool zoomDependent, double shapeDetectionTime, bool shapeDetectionEnabled, PenProperty property + String name, String displayIcon,@IdJsonConverter() String? id, bool zoomDependent, double shapeDetectionTime, bool shapeDetectionEnabled, bool combineHighlights, PenProperty property }); @@ -714,7 +715,7 @@ class _$PenToolCopyWithImpl<$Res> /// Create a copy of Tool /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? displayIcon = null,Object? id = freezed,Object? zoomDependent = null,Object? shapeDetectionTime = null,Object? shapeDetectionEnabled = null,Object? property = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? displayIcon = null,Object? id = freezed,Object? zoomDependent = null,Object? shapeDetectionTime = null,Object? shapeDetectionEnabled = null,Object? combineHighlights = null,Object? property = freezed,}) { return _then(PenTool( name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,displayIcon: null == displayIcon ? _self.displayIcon : displayIcon // ignore: cast_nullable_to_non_nullable @@ -722,6 +723,7 @@ as String,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_null as String?,zoomDependent: null == zoomDependent ? _self.zoomDependent : zoomDependent // ignore: cast_nullable_to_non_nullable as bool,shapeDetectionTime: null == shapeDetectionTime ? _self.shapeDetectionTime : shapeDetectionTime // ignore: cast_nullable_to_non_nullable as double,shapeDetectionEnabled: null == shapeDetectionEnabled ? _self.shapeDetectionEnabled : shapeDetectionEnabled // ignore: cast_nullable_to_non_nullable +as bool,combineHighlights: null == combineHighlights ? _self.combineHighlights : combineHighlights // ignore: cast_nullable_to_non_nullable as bool,property: freezed == property ? _self.property : property // ignore: cast_nullable_to_non_nullable as PenProperty, )); diff --git a/api/lib/src/models/tool.g.dart b/api/lib/src/models/tool.g.dart index c5bb540fb3d0..1bca5f06397d 100644 --- a/api/lib/src/models/tool.g.dart +++ b/api/lib/src/models/tool.g.dart @@ -163,6 +163,7 @@ PenTool _$PenToolFromJson(Map json) => PenTool( zoomDependent: json['zoomDependent'] as bool? ?? false, shapeDetectionTime: (json['shapeDetectionTime'] as num?)?.toDouble() ?? 0.5, shapeDetectionEnabled: json['shapeDetectionEnabled'] as bool? ?? false, + combineHighlights: json['combineHighlights'] as bool? ?? false, property: json['property'] == null ? const PenProperty() : PenProperty.fromJson( @@ -178,6 +179,7 @@ Map _$PenToolToJson(PenTool instance) => { 'zoomDependent': instance.zoomDependent, 'shapeDetectionTime': instance.shapeDetectionTime, 'shapeDetectionEnabled': instance.shapeDetectionEnabled, + 'combineHighlights': instance.combineHighlights, 'property': instance.property.toJson(), 'type': instance.$type, }; diff --git a/api/test/data_test.dart b/api/test/data_test.dart index 8e2451c00a00..8b44ac86e0ef 100644 --- a/api/test/data_test.dart +++ b/api/test/data_test.dart @@ -11,6 +11,32 @@ DocumentPage _pageWithLayer(String layerId) => DocumentPage(layers: [DocumentLayer(id: layerId)]); void main() { + group('Highlighter options', () { + test('pen tool options round-trip through JSON', () { + final tool = PenTool(id: 'highlighter', combineHighlights: true); + + final decoded = Tool.fromJson(tool.toJson()) as PenTool; + + expect(decoded.combineHighlights, isTrue); + }); + + test('pen element combine id round-trips through JSON', () { + final element = PenElement(id: 'stroke', combineId: 'highlighter'); + + final decoded = PadElement.fromJson(element.toJson()) as PenElement; + + expect(decoded.combineId, 'highlighter'); + }); + + test('new options remain disabled for old JSON', () { + final tool = Tool.fromJson({'type': 'pen'}) as PenTool; + final element = PadElement.fromJson({'type': 'pen'}) as PenElement; + + expect(tool.combineHighlights, isFalse); + expect(element.combineId, isNull); + }); + }); + group('AssetFileType helpers', () { test('fromFileExtension handles uppercase and dotted PDF extensions', () { expect(AssetFileTypeHelper.fromFileExtension('PDF'), AssetFileType.pdf); diff --git a/app/lib/cubits/current_index.dart b/app/lib/cubits/current_index.dart index 958533a5151e..377a4a1c5b50 100644 --- a/app/lib/cubits/current_index.dart +++ b/app/lib/cubits/current_index.dart @@ -2563,11 +2563,20 @@ class CurrentIndexCubit extends Cubit { if (blocState is! DocumentLoadSuccess) return; state.handler.onDocumentUpdated(blocState, oldState); + final addsCombinedHighlight = addedElements.any( + (renderer) => + renderer is PenRenderer && renderer.element.combineId != null, + ); if (replacedElements != null) { await replaceUnbaked(blocState, [ ...replacedElements, ...addedElements, ], backgrounds: backgrounds); + } else if (addsCombinedHighlight) { + await this.unbake( + blocState, + unbakedElements: [...renderers, ...addedElements], + ); } else if (unbake) { await this.unbake(blocState, backgrounds: backgrounds); } else if (backgrounds != null) { diff --git a/app/lib/handlers/pen.dart b/app/lib/handlers/pen.dart index 9e0979c05b31..768ad3058562 100644 --- a/app/lib/handlers/pen.dart +++ b/app/lib/handlers/pen.dart @@ -188,6 +188,7 @@ class PenHandler extends Handler with ColoredHandler { elements[pointer] = PenElement( id: createUniqueId(), zoom: transform.size, + combineId: data.combineHighlights ? data.id : null, collection: state.currentCollection, property: data.property.copyWith( strokeWidth: data.property.strokeWidth / zoom, diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index f9d9fb1d04c0..7610489d6f25 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -123,6 +123,7 @@ }, "defaultPalette": "Default palette", "highlighter": "Highlighter", + "combineHighlights": "Combine highlights", "add": "Add", "@add": { "description": "Add action" diff --git a/app/lib/renderers/elements/pen.dart b/app/lib/renderers/elements/pen.dart index fd85f88ea884..bef67fe63520 100644 --- a/app/lib/renderers/elements/pen.dart +++ b/app/lib/renderers/elements/pen.dart @@ -43,17 +43,17 @@ class PenRenderer extends Renderer { if (property.paint.previewColor.a > 0) { final outlinePoints = _getOutlinePoints(); if (outlinePoints.isNotEmpty) { - _cachedStrokePath = Path(); + final strokePath = Path(); if (outlinePoints.length < 2) { - _cachedStrokePath!.addOval( + strokePath.addOval( Rect.fromCircle(center: outlinePoints[0], radius: 1), ); } else { - _cachedStrokePath!.moveTo(outlinePoints[0].dx, outlinePoints[0].dy); + strokePath.moveTo(outlinePoints[0].dx, outlinePoints[0].dy); for (int i = 1; i < outlinePoints.length - 1; ++i) { final p0 = outlinePoints[i]; final p1 = outlinePoints[i + 1]; - _cachedStrokePath!.quadraticBezierTo( + strokePath.quadraticBezierTo( p0.dx, p0.dy, (p0.dx + p1.dx) / 2, @@ -61,6 +61,7 @@ class PenRenderer extends Renderer { ); } } + _cachedStrokePath = strokePath; } } } @@ -166,6 +167,14 @@ class PenRenderer extends Renderer { ColorScheme? colorScheme, bool foreground = false, ]) { + _build(canvas); + } + + void buildCombined(Canvas canvas) { + _build(canvas, BlendMode.src); + } + + void _build(Canvas canvas, [BlendMode? blendMode]) { final points = element.points; if (points.isEmpty) return; final property = element.property; @@ -175,19 +184,21 @@ class PenRenderer extends Renderer { } if (property.fillPaint.previewColor.a > 0 && _cachedFillPath != null) { - final paint = _fillPaint.build( - property.fillPaint, - rect, - style: PaintingStyle.fill, - )..strokeCap = StrokeCap.round; + final paint = + _fillPaint.build(property.fillPaint, rect, style: PaintingStyle.fill) + ..strokeCap = StrokeCap.round + ..blendMode = blendMode ?? BlendMode.srcOver; canvas.drawPath(_cachedFillPath!, paint); } if (property.paint.previewColor.a > 0 && _cachedStrokePath != null) { - final paint = _strokePaint.build( - property.paint, - expandedRect, - style: PaintingStyle.fill, - )..strokeCap = StrokeCap.round; + final paint = + _strokePaint.build( + property.paint, + expandedRect, + style: PaintingStyle.fill, + ) + ..strokeCap = StrokeCap.round + ..blendMode = blendMode ?? BlendMode.srcOver; canvas.drawPath(_cachedStrokePath!, paint); } } @@ -196,7 +207,6 @@ class PenRenderer extends Renderer { final currentZoom = element.zoom ?? kMaxZoom; final property = element.property; final center = rect.center; - // 1. Get the outline points from the input points var outlinePoints = freehand.getStroke( element.points .map((e) => e.scale(currentZoom, center)) @@ -211,11 +221,9 @@ class PenRenderer extends Renderer { ), ); - // Unscale the points - outlinePoints = outlinePoints + return outlinePoints .map((e) => e.scaleFromCenter(1 / currentZoom, center)) .toList(); - return outlinePoints; } @override @@ -256,12 +264,10 @@ class PenRenderer extends Renderer { final first = outlinePoints.first; path += 'M ${first.roundedX()} ${first.roundedY()}'; - for (int i = 1; i < outlinePoints.length - 1; ++i) { - final p0 = outlinePoints[i]; - final p1 = outlinePoints[i + 1]; - path += - ' Q ${p0.roundedX()} ${p0.roundedY()} ${p0.roundedBetweenX(p1)} ${p0.roundedBetweenY(p1)}'; + for (final point in outlinePoints.sublist(1)) { + path += ' L ${point.roundedX()} ${point.roundedY()}'; } + path += ' Z'; xml.getElement('svg')?.createElement('path') ?..setAttribute('d', path) diff --git a/app/lib/selections/tools/pen.dart b/app/lib/selections/tools/pen.dart index 0bf7bb7fa567..478e05ec21b7 100644 --- a/app/lib/selections/tools/pen.dart +++ b/app/lib/selections/tools/pen.dart @@ -106,6 +106,16 @@ class PenToolSelection extends ToolSelection { .toList(), ), ), + CheckboxListTile( + value: selected.first.combineHighlights, + title: Text(AppLocalizations.of(context).combineHighlights), + onChanged: (value) => update( + context, + selected + .map((e) => e.copyWith(combineHighlights: value ?? false)) + .toList(), + ), + ), const SizedBox(height: 16), _ShapeDetectionView(selected: selected, update: update), const SizedBox(height: 16), diff --git a/app/lib/view_painter.dart b/app/lib/view_painter.dart index 00f7c8ccb6ec..7a14b39d7a76 100644 --- a/app/lib/view_painter.dart +++ b/app/lib/view_painter.dart @@ -15,6 +15,102 @@ import 'package:material_leap/material_leap.dart'; import 'cubits/transform.dart'; import 'selections/selection.dart'; +void _paintRenderer( + Canvas canvas, + Size size, + NoteData document, + DocumentPage page, + DocumentInfo info, + CameraTransform transform, + ColorScheme? colorScheme, + Renderer renderer, { + bool foreground = false, + bool combined = false, +}) { + canvas.save(); + final center = renderer.rect?.center; + if (center != null) { + canvas.translate(center.dx, center.dy); + } + canvas.rotate(renderer.rotation * (pi / 180)); + if (center != null) { + canvas.translate(-center.dx, -center.dy); + } + if (combined && renderer is PenRenderer) { + renderer.buildCombined(canvas); + } else { + renderer.build( + canvas, + size, + document, + page, + info, + transform, + colorScheme, + foreground, + ); + } + canvas.restore(); +} + +void _paintRenderers( + Canvas canvas, + Size size, + NoteData document, + DocumentPage page, + DocumentInfo info, + CameraTransform transform, + ColorScheme? colorScheme, + Iterable renderers, { + bool foreground = false, +}) { + final rendererList = renderers.toList(); + final groups = >{}; + for (final renderer in rendererList.whereType()) { + final combineId = renderer.element.combineId; + if (combineId != null) { + groups.putIfAbsent(combineId, () => []).add(renderer); + } + } + final paintedGroups = {}; + for (final renderer in rendererList) { + final combineId = renderer is PenRenderer + ? renderer.element.combineId + : null; + if (combineId == null) { + _paintRenderer( + canvas, + size, + document, + page, + info, + transform, + colorScheme, + renderer, + foreground: foreground, + ); + continue; + } + if (!paintedGroups.add(combineId)) continue; + canvas.saveLayer(null, Paint()); + for (final groupedRenderer in groups[combineId]!) { + _paintRenderer( + canvas, + size, + document, + page, + info, + transform, + colorScheme, + groupedRenderer, + foreground: foreground, + combined: true, + ); + } + canvas.restore(); + } +} + class ForegroundPainter extends CustomPainter { final ColorScheme colorScheme; final NoteData document; @@ -42,34 +138,17 @@ class ForegroundPainter extends CustomPainter { if (renderers.isEmpty && sel == null) return; canvas.scale(transform.size); canvas.translate(-transform.position.dx, -transform.position.dy); - for (var renderer in renderers) { - final center = renderer.rect?.center; - final radian = renderer.rotation * (pi / 180); - if (center != null) { - canvas.translate(center.dx, center.dy); - } - canvas.rotate(radian); - if (center != null) { - canvas.translate(-center.dx, -center.dy); - } - renderer.build( - canvas, - size, - document, - page, - info, - transform, - colorScheme, - true, - ); - if (center != null) { - canvas.translate(center.dx, center.dy); - } - canvas.rotate(-radian); - if (center != null) { - canvas.translate(-center.dx, -center.dy); - } - } + _paintRenderers( + canvas, + size, + document, + page, + info, + transform, + colorScheme, + renderers, + foreground: true, + ); if (sel is ElementSelection) { _drawSelection(canvas, size, sel); } @@ -208,39 +287,21 @@ class ViewPainter extends CustomPainter { canvas.scale(transform.size, transform.size); canvas.translate(-transform.position.dx, -transform.position.dy); - final renderers = cameraViewport.visibleUnbakedElements; - for (final renderer in renderers) { + final renderers = cameraViewport.visibleUnbakedElements.where((renderer) { final state = cameraViewport.rendererStates[renderer.id]; - if (!(invisibleLayers?.contains(renderer.layer) ?? false) && - state != RendererState.hidden) { - final center = renderer.rect?.center; - final radian = renderer.rotation * (pi / 180); - if (center != null) { - canvas.translate(center.dx, center.dy); - } - canvas.rotate(radian); - if (center != null) { - canvas.translate(-center.dx, -center.dy); - } - renderer.build( - canvas, - size, - document, - page, - info, - transform, - colorScheme, - false, - ); - if (center != null) { - canvas.translate(center.dx, center.dy); - } - canvas.rotate(-radian); - if (center != null) { - canvas.translate(-center.dx, -center.dy); - } - } - } + return !(invisibleLayers?.contains(renderer.layer) ?? false) && + state != RendererState.hidden; + }); + _paintRenderers( + canvas, + size, + document, + page, + info, + transform, + colorScheme, + renderers, + ); canvas.translate(transform.position.dx, transform.position.dy); canvas.scale(1 / transform.size, 1 / transform.size); final aboveLayerImage = cameraViewport.aboveLayerImage; diff --git a/app/test/bloc/document_bloc_test.dart b/app/test/bloc/document_bloc_test.dart index ea4406e03693..f5a77cc1c59a 100644 --- a/app/test/bloc/document_bloc_test.dart +++ b/app/test/bloc/document_bloc_test.dart @@ -600,6 +600,75 @@ void main() { expect(currentIndexCubit.state.buttons, isNull); }); + test('adding a combined highlight immediately unbakes its group', () async { + await bloc.close(); + await currentIndexCubit.close(); + + final existingElement = PenElement( + id: 'existing', + combineId: 'highlighter', + points: const [PathPoint(10, 20), PathPoint(40, 20)], + ); + final existingRenderer = Renderer.fromInstance(existingElement); + final page = DocumentPage( + layers: [ + DocumentLayer(id: 'layer', content: [existingElement]), + ], + ); + var data = NoteData(Archive()); + final (nextData, pageName) = data.setPage(page, 'Page 1'); + data = nextData; + currentIndexCubit = CurrentIndexCubit( + settingsCubit, + TransformCubit(1), + CameraViewport.unbaked( + unbakedElements: [existingRenderer], + visibleElements: [existingRenderer], + visibleUnbakedElements: [existingRenderer], + width: 100, + height: 100, + ), + ); + bloc = DocumentBloc( + fileSystem, + currentIndexCubit, + windowCubit, + data, + const AssetLocation(path: 'test-note.bfly'), + null, + page, + pageName, + ); + await currentIndexCubit.bake( + bloc.state as DocumentLoadSuccess, + viewportSize: const Size(100, 100), + pixelRatio: 1, + reset: true, + ); + expect(currentIndexCubit.state.cameraViewport.bakedElements, isNotEmpty); + + bloc.add( + ElementsCreated([ + PenElement( + id: 'new', + combineId: 'highlighter', + points: const [PathPoint(20, 20), PathPoint(50, 20)], + ), + ]), + ); + await _settleBlocEvents(); + + final viewport = currentIndexCubit.state.cameraViewport; + expect(viewport.bakedElements, isEmpty); + expect( + viewport.unbakedElements.whereType().map( + (renderer) => renderer.element.combineId, + ), + everyElement('highlighter'), + ); + expect(viewport.unbakedElements, hasLength(2)); + }); + test('bake records only elements visible in the current viewport', () async { await bloc.close(); await currentIndexCubit.close(); diff --git a/app/test/renderers/highlighter_renderer_test.dart b/app/test/renderers/highlighter_renderer_test.dart new file mode 100644 index 000000000000..8112bc302d6d --- /dev/null +++ b/app/test/renderers/highlighter_renderer_test.dart @@ -0,0 +1,80 @@ +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:archive/archive.dart'; +import 'package:butterfly/models/viewport.dart'; +import 'package:butterfly/renderers/renderer.dart'; +import 'package:butterfly/view_painter.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:dart_leap/dart_leap.dart'; +import 'package:flutter_test/flutter_test.dart'; + +const _size = ui.Size(100, 100); +const _strokeRect = ui.Rect.fromLTRB(20, 50, 80, 50); +const _expandedStrokeRect = ui.Rect.fromLTRB(10, 40, 90, 60); +const _property = PenProperty( + strokeWidth: 20, + thinning: 0, + paint: ElementPaint.solid(color: SRGBColor(0x80FF0000)), +); + +PenRenderer _renderer(String id, {String? combineId}) => PenRenderer( + PenElement( + id: id, + combineId: combineId, + property: _property, + points: const [ + PathPoint(20, 50), + PathPoint(40, 50), + PathPoint(60, 50), + PathPoint(80, 50), + ], + ), + null, + _strokeRect, + _expandedStrokeRect, +); + +Future _render(List renderers) async { + final recorder = ui.PictureRecorder(); + final canvas = ui.Canvas(recorder); + ViewPainter( + NoteData(Archive()), + const DocumentPage(), + const DocumentInfo(), + cameraViewport: CameraViewport.unbaked( + unbakedElements: renderers, + visibleElements: renderers, + visibleUnbakedElements: renderers, + ), + ).paint(canvas, _size); + final picture = recorder.endRecording(); + ui.Image? image; + try { + image = await picture.toImage(_size.width.toInt(), _size.height.toInt()); + final data = await image.toByteData(format: ui.ImageByteFormat.rawRgba); + return data!.buffer.asUint8List(); + } finally { + image?.dispose(); + picture.dispose(); + } +} + +int _greenAtCenter(Uint8List pixels) { + const x = 50, y = 50; + return pixels[(y * _size.width.toInt() + x) * 4 + 1]; +} + +void main() { + test('combined highlights do not darken their overlap', () async { + final single = await _render([_renderer('single')]); + final combined = await _render([ + _renderer('first', combineId: 'highlighter'), + _renderer('second', combineId: 'highlighter'), + ]); + final separate = await _render([_renderer('first'), _renderer('second')]); + + expect(_greenAtCenter(combined), _greenAtCenter(single)); + expect(_greenAtCenter(separate), lessThan(_greenAtCenter(single))); + }); +} From 70ba888a012ad6282d8cd1d0b42394c573e532d3 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 23 Jun 2026 14:03:13 +0200 Subject: [PATCH 009/117] Change combineHighlighter to combinePaths --- api/lib/src/models/tool.dart | 2 +- api/lib/src/models/tool.freezed.dart | 12 +- api/lib/src/models/tool.g.dart | 4 +- api/pubspec.lock | 1278 +++++++++++++------------- api/test/data_test.dart | 6 +- app/lib/handlers/pen.dart | 2 +- app/lib/l10n/app_en.arb | 2 +- app/lib/selections/tools/pen.dart | 6 +- app/pubspec.lock | 2 +- app/pubspec.yaml | 2 +- metadata/en-US/changelogs/187.txt | 1 + 11 files changed, 659 insertions(+), 658 deletions(-) diff --git a/api/lib/src/models/tool.dart b/api/lib/src/models/tool.dart index b217d82028a0..1f2546945f18 100644 --- a/api/lib/src/models/tool.dart +++ b/api/lib/src/models/tool.dart @@ -133,7 +133,7 @@ sealed class Tool extends PackAsset with _$Tool { @Default(false) bool zoomDependent, @Default(0.5) double shapeDetectionTime, @Default(false) bool shapeDetectionEnabled, - @Default(false) bool combineHighlights, + @Default(false) bool combinePaths, @Default(PenProperty()) PenProperty property, }) = PenTool; diff --git a/api/lib/src/models/tool.freezed.dart b/api/lib/src/models/tool.freezed.dart index 8954c5fe56a6..87e6c672c732 100644 --- a/api/lib/src/models/tool.freezed.dart +++ b/api/lib/src/models/tool.freezed.dart @@ -656,7 +656,7 @@ $NamedItemCopyWith? get styleSheet { @JsonSerializable() class PenTool extends Tool { - PenTool({this.name = '', this.displayIcon = '', @IdJsonConverter() this.id, this.zoomDependent = false, this.shapeDetectionTime = 0.5, this.shapeDetectionEnabled = false, this.combineHighlights = false, this.property = const PenProperty(), final String? $type}): $type = $type ?? 'pen',super._(); + PenTool({this.name = '', this.displayIcon = '', @IdJsonConverter() this.id, this.zoomDependent = false, this.shapeDetectionTime = 0.5, this.shapeDetectionEnabled = false, this.combinePaths = false, this.property = const PenProperty(), final String? $type}): $type = $type ?? 'pen',super._(); factory PenTool.fromJson(Map json) => _$PenToolFromJson(json); @override@JsonKey() final String name; @@ -665,7 +665,7 @@ class PenTool extends Tool { @JsonKey() final bool zoomDependent; @JsonKey() final double shapeDetectionTime; @JsonKey() final bool shapeDetectionEnabled; -@JsonKey() final bool combineHighlights; +@JsonKey() final bool combinePaths; @JsonKey() final PenProperty property; @JsonKey(name: 'type') @@ -687,7 +687,7 @@ Map toJson() { @override String toString() { - return 'Tool.pen(name: $name, displayIcon: $displayIcon, id: $id, zoomDependent: $zoomDependent, shapeDetectionTime: $shapeDetectionTime, shapeDetectionEnabled: $shapeDetectionEnabled, combineHighlights: $combineHighlights, property: $property)'; + return 'Tool.pen(name: $name, displayIcon: $displayIcon, id: $id, zoomDependent: $zoomDependent, shapeDetectionTime: $shapeDetectionTime, shapeDetectionEnabled: $shapeDetectionEnabled, combinePaths: $combinePaths, property: $property)'; } @@ -698,7 +698,7 @@ abstract mixin class $PenToolCopyWith<$Res> implements $ToolCopyWith<$Res> { factory $PenToolCopyWith(PenTool value, $Res Function(PenTool) _then) = _$PenToolCopyWithImpl; @override @useResult $Res call({ - String name, String displayIcon,@IdJsonConverter() String? id, bool zoomDependent, double shapeDetectionTime, bool shapeDetectionEnabled, bool combineHighlights, PenProperty property + String name, String displayIcon,@IdJsonConverter() String? id, bool zoomDependent, double shapeDetectionTime, bool shapeDetectionEnabled, bool combinePaths, PenProperty property }); @@ -715,7 +715,7 @@ class _$PenToolCopyWithImpl<$Res> /// Create a copy of Tool /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? displayIcon = null,Object? id = freezed,Object? zoomDependent = null,Object? shapeDetectionTime = null,Object? shapeDetectionEnabled = null,Object? combineHighlights = null,Object? property = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? displayIcon = null,Object? id = freezed,Object? zoomDependent = null,Object? shapeDetectionTime = null,Object? shapeDetectionEnabled = null,Object? combinePaths = null,Object? property = freezed,}) { return _then(PenTool( name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,displayIcon: null == displayIcon ? _self.displayIcon : displayIcon // ignore: cast_nullable_to_non_nullable @@ -723,7 +723,7 @@ as String,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_null as String?,zoomDependent: null == zoomDependent ? _self.zoomDependent : zoomDependent // ignore: cast_nullable_to_non_nullable as bool,shapeDetectionTime: null == shapeDetectionTime ? _self.shapeDetectionTime : shapeDetectionTime // ignore: cast_nullable_to_non_nullable as double,shapeDetectionEnabled: null == shapeDetectionEnabled ? _self.shapeDetectionEnabled : shapeDetectionEnabled // ignore: cast_nullable_to_non_nullable -as bool,combineHighlights: null == combineHighlights ? _self.combineHighlights : combineHighlights // ignore: cast_nullable_to_non_nullable +as bool,combinePaths: null == combinePaths ? _self.combinePaths : combinePaths // ignore: cast_nullable_to_non_nullable as bool,property: freezed == property ? _self.property : property // ignore: cast_nullable_to_non_nullable as PenProperty, )); diff --git a/api/lib/src/models/tool.g.dart b/api/lib/src/models/tool.g.dart index 1bca5f06397d..3d121841ad77 100644 --- a/api/lib/src/models/tool.g.dart +++ b/api/lib/src/models/tool.g.dart @@ -163,7 +163,7 @@ PenTool _$PenToolFromJson(Map json) => PenTool( zoomDependent: json['zoomDependent'] as bool? ?? false, shapeDetectionTime: (json['shapeDetectionTime'] as num?)?.toDouble() ?? 0.5, shapeDetectionEnabled: json['shapeDetectionEnabled'] as bool? ?? false, - combineHighlights: json['combineHighlights'] as bool? ?? false, + combinePaths: json['combinePaths'] as bool? ?? false, property: json['property'] == null ? const PenProperty() : PenProperty.fromJson( @@ -179,7 +179,7 @@ Map _$PenToolToJson(PenTool instance) => { 'zoomDependent': instance.zoomDependent, 'shapeDetectionTime': instance.shapeDetectionTime, 'shapeDetectionEnabled': instance.shapeDetectionEnabled, - 'combineHighlights': instance.combineHighlights, + 'combinePaths': instance.combinePaths, 'property': instance.property.toJson(), 'type': instance.$type, }; diff --git a/api/pubspec.lock b/api/pubspec.lock index bbfc0f0deb4a..bfe2e36c3b56 100644 --- a/api/pubspec.lock +++ b/api/pubspec.lock @@ -1,639 +1,639 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: "3b19a47f6ea7c2632760777c78174f47f6aec1e05f0cd611380d4593b8af1dbc" - url: "https://pub.dev" - source: hosted - version: "96.0.0" - analyzer: - dependency: "direct dev" - description: - name: analyzer - sha256: "0c516bc4ad36a1a75759e54d5047cb9d15cded4459df01aa35a0b5ec7db2c2a0" - url: "https://pub.dev" - source: hosted - version: "10.2.0" - archive: - dependency: "direct main" - description: - name: archive - sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff - url: "https://pub.dev" - source: hosted - version: "4.0.9" - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 - url: "https://pub.dev" - source: hosted - version: "2.13.1" - bloc: - dependency: transitive - description: - name: bloc - sha256: e03b235924e4f509c27b5d6b2f949200e0a91149a9818b4f65eeb56662b75413 - url: "https://pub.dev" - source: hosted - version: "9.2.1" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - build: - dependency: transitive - description: - name: build - sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10 - url: "https://pub.dev" - source: hosted - version: "4.0.6" - build_config: - dependency: transitive - description: - name: build_config - sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" - url: "https://pub.dev" - source: hosted - version: "1.3.0" - build_daemon: - dependency: transitive - description: - name: build_daemon - sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 - url: "https://pub.dev" - source: hosted - version: "4.1.1" - build_runner: - dependency: "direct dev" - description: - name: build_runner - sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6" - url: "https://pub.dev" - source: hosted - version: "2.15.0" - built_collection: - dependency: transitive - description: - name: built_collection - sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.dev" - source: hosted - version: "5.1.1" - built_value: - dependency: transitive - description: - name: built_value - sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" - url: "https://pub.dev" - source: hosted - version: "8.12.6" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.dev" - source: hosted - version: "2.0.4" - cli_config: - dependency: transitive - description: - name: cli_config - sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec - url: "https://pub.dev" - source: hosted - version: "0.2.0" - collection: - dependency: "direct main" - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - coverage: - dependency: transitive - description: - name: coverage - sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d" - url: "https://pub.dev" - source: hosted - version: "1.15.1" - crypto: - dependency: transitive - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - dart_leap: - dependency: "direct main" - description: - path: "packages/dart_leap" - ref: b7787191b0705ff0a22149409b1b360468d9e06d - resolved-ref: b7787191b0705ff0a22149409b1b360468d9e06d - url: "https://github.com/LinwoodDev/dart_pkgs.git" - source: git - version: "1.0.0" - dart_mappable: - dependency: transitive - description: - name: dart_mappable - sha256: "960746478faaa68ed6b9d3c6fd03c87c7b8614e6c33e75fe1b0c6d7a60adcf29" - url: "https://pub.dev" - source: hosted - version: "4.8.0" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2" - url: "https://pub.dev" - source: hosted - version: "3.1.7" - ffi: - dependency: transitive - description: - name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - freezed: - dependency: "direct dev" - description: - name: freezed - sha256: f23ea33b3863f119b58ed1b586e881a46bd28715ddcc4dbc33104524e3434131 - url: "https://pub.dev" - source: hosted - version: "3.2.5" - freezed_annotation: - dependency: "direct main" - description: - name: freezed_annotation - sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 - url: "https://pub.dev" - source: hosted - version: "4.0.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - graphs: - dependency: transitive - description: - name: graphs - sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 - url: "https://pub.dev" - source: hosted - version: "3.2.2" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - io: - dependency: transitive - description: - name: io - sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b - url: "https://pub.dev" - source: hosted - version: "1.0.5" - json_annotation: - dependency: "direct main" - description: - name: json_annotation - sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" - url: "https://pub.dev" - source: hosted - version: "4.12.0" - json_serializable: - dependency: "direct dev" - description: - name: json_serializable - sha256: ffcd10cde35a93b2abbbcc26bd9971f4ca93763e8abe78d855e3c4177797e501 - url: "https://pub.dev" - source: hosted - version: "6.14.0" - lints: - dependency: "direct dev" - description: - name: lints - sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - lw_file_system_api: - dependency: "direct main" - description: - path: "packages/lw_file_system_api" - ref: "37594211585889e35e65dad382d4fdcf29ac69a0" - resolved-ref: "37594211585889e35e65dad382d4fdcf29ac69a0" - url: "https://github.com/LinwoodDev/dart_pkgs.git" - source: git - version: "1.0.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" - url: "https://pub.dev" - source: hosted - version: "0.12.20" - meta: - dependency: transitive - description: - name: meta - sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d - url: "https://pub.dev" - source: hosted - version: "1.18.3" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - node_preamble: - dependency: transitive - description: - name: node_preamble - sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - package_config: - dependency: transitive - description: - name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" - source: hosted - version: "2.2.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" - url: "https://pub.dev" - source: hosted - version: "7.0.2" - pool: - dependency: transitive - description: - name: pool - sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" - url: "https://pub.dev" - source: hosted - version: "1.5.2" - posix: - dependency: transitive - description: - name: posix - sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" - url: "https://pub.dev" - source: hosted - version: "6.5.0" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" - url: "https://pub.dev" - source: hosted - version: "1.5.0" - replay_bloc: - dependency: "direct main" - description: - name: replay_bloc - sha256: "1e9bc379bd64a0625ccf080a1618f3a9514ff3a998b7dd7a306379d4303318f3" - url: "https://pub.dev" - source: hosted - version: "0.3.0" - rxdart: - dependency: "direct main" - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" - shelf: - dependency: transitive - description: - name: shelf - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 - url: "https://pub.dev" - source: hosted - version: "1.4.2" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - shelf_static: - dependency: transitive - description: - name: shelf_static - sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 - url: "https://pub.dev" - source: hosted - version: "1.1.3" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - source_gen: - dependency: transitive - description: - name: source_gen - sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02 - url: "https://pub.dev" - source: hosted - version: "4.2.3" - source_helper: - dependency: transitive - description: - name: source_helper - sha256: "4227d54ceefd0bb8ca4c8fcb96e1719dc53f1ee1b6e2ca9d7a6069da160e4eae" - url: "https://pub.dev" - source: hosted - version: "1.3.12" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b - url: "https://pub.dev" - source: hosted - version: "2.1.2" - source_maps: - dependency: transitive - description: - name: source_maps - sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" - url: "https://pub.dev" - source: hosted - version: "0.10.13" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" - source: hosted - version: "1.10.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 - url: "https://pub.dev" - source: hosted - version: "2.1.1" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test: - dependency: "direct dev" - description: - name: test - sha256: ca578dc12bb8b2f40b67b7d3bd2fac4f31c01a6ff7130a14e2597b919934507f - url: "https://pub.dev" - source: hosted - version: "1.31.1" - test_api: - dependency: transitive - description: - name: test_api - sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" - url: "https://pub.dev" - source: hosted - version: "0.7.12" - test_core: - dependency: transitive - description: - name: test_core - sha256: d2e98ec12998368dc59ddd47ab709f2cd55acd6b66dc7db764455a44082f4bc5 - url: "https://pub.dev" - source: hosted - version: "0.6.18" - type_plus: - dependency: transitive - description: - name: type_plus - sha256: d5d1019471f0d38b91603adb9b5fd4ce7ab903c879d2fbf1a3f80a630a03fcc9 - url: "https://pub.dev" - source: hosted - version: "2.1.1" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - uuid: - dependency: "direct main" - description: - name: uuid - sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" - url: "https://pub.dev" - source: hosted - version: "4.5.3" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" - url: "https://pub.dev" - source: hosted - version: "15.2.0" - watcher: - dependency: transitive - description: - name: watcher - sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - web_socket: - dependency: transitive - description: - name: web_socket - sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 - url: "https://pub.dev" - source: hosted - version: "3.0.3" - webkit_inspection_protocol: - dependency: transitive - description: - name: webkit_inspection_protocol - sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - xml: - dependency: "direct main" - description: - name: xml - sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" - url: "https://pub.dev" - source: hosted - version: "7.0.1" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.11.0 <4.0.0" +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "3b19a47f6ea7c2632760777c78174f47f6aec1e05f0cd611380d4593b8af1dbc" + url: "https://pub.dev" + source: hosted + version: "96.0.0" + analyzer: + dependency: "direct dev" + description: + name: analyzer + sha256: "0c516bc4ad36a1a75759e54d5047cb9d15cded4459df01aa35a0b5ec7db2c2a0" + url: "https://pub.dev" + source: hosted + version: "10.2.0" + archive: + dependency: "direct main" + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + bloc: + dependency: transitive + description: + name: bloc + sha256: e03b235924e4f509c27b5d6b2f949200e0a91149a9818b4f65eeb56662b75413 + url: "https://pub.dev" + source: hosted + version: "9.2.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10 + url: "https://pub.dev" + source: hosted + version: "4.0.6" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + url: "https://pub.dev" + source: hosted + version: "4.1.1" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6" + url: "https://pub.dev" + source: hosted + version: "2.15.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" + url: "https://pub.dev" + source: hosted + version: "8.12.6" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" + collection: + dependency: "direct main" + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d" + url: "https://pub.dev" + source: hosted + version: "1.15.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + dart_leap: + dependency: "direct main" + description: + path: "packages/dart_leap" + ref: b7787191b0705ff0a22149409b1b360468d9e06d + resolved-ref: b7787191b0705ff0a22149409b1b360468d9e06d + url: "https://github.com/LinwoodDev/dart_pkgs.git" + source: git + version: "1.0.0" + dart_mappable: + dependency: transitive + description: + name: dart_mappable + sha256: "960746478faaa68ed6b9d3c6fd03c87c7b8614e6c33e75fe1b0c6d7a60adcf29" + url: "https://pub.dev" + source: hosted + version: "4.8.0" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2" + url: "https://pub.dev" + source: hosted + version: "3.1.7" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: f23ea33b3863f119b58ed1b586e881a46bd28715ddcc4dbc33104524e3434131 + url: "https://pub.dev" + source: hosted + version: "3.2.5" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" + url: "https://pub.dev" + source: hosted + version: "4.12.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: ffcd10cde35a93b2abbbcc26bd9971f4ca93763e8abe78d855e3c4177797e501 + url: "https://pub.dev" + source: hosted + version: "6.14.0" + lints: + dependency: "direct dev" + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + lw_file_system_api: + dependency: "direct main" + description: + path: "packages/lw_file_system_api" + ref: "37594211585889e35e65dad382d4fdcf29ac69a0" + resolved-ref: "37594211585889e35e65dad382d4fdcf29ac69a0" + url: "https://github.com/LinwoodDev/dart_pkgs.git" + source: git + version: "1.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" + url: "https://pub.dev" + source: hosted + version: "0.12.20" + meta: + dependency: transitive + description: + name: meta + sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d + url: "https://pub.dev" + source: hosted + version: "1.18.3" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + replay_bloc: + dependency: "direct main" + description: + name: replay_bloc + sha256: "1e9bc379bd64a0625ccf080a1618f3a9514ff3a998b7dd7a306379d4303318f3" + url: "https://pub.dev" + source: hosted + version: "0.3.0" + rxdart: + dependency: "direct main" + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02 + url: "https://pub.dev" + source: hosted + version: "4.2.3" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "4227d54ceefd0bb8ca4c8fcb96e1719dc53f1ee1b6e2ca9d7a6069da160e4eae" + url: "https://pub.dev" + source: hosted + version: "1.3.12" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test: + dependency: "direct dev" + description: + name: test + sha256: ca578dc12bb8b2f40b67b7d3bd2fac4f31c01a6ff7130a14e2597b919934507f + url: "https://pub.dev" + source: hosted + version: "1.31.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" + url: "https://pub.dev" + source: hosted + version: "0.7.12" + test_core: + dependency: transitive + description: + name: test_core + sha256: d2e98ec12998368dc59ddd47ab709f2cd55acd6b66dc7db764455a44082f4bc5 + url: "https://pub.dev" + source: hosted + version: "0.6.18" + type_plus: + dependency: transitive + description: + name: type_plus + sha256: d5d1019471f0d38b91603adb9b5fd4ce7ab903c879d2fbf1a3f80a630a03fcc9 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + xml: + dependency: "direct main" + description: + name: xml + sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.11.0 <4.0.0" diff --git a/api/test/data_test.dart b/api/test/data_test.dart index 8b44ac86e0ef..ba4b2774986f 100644 --- a/api/test/data_test.dart +++ b/api/test/data_test.dart @@ -13,11 +13,11 @@ DocumentPage _pageWithLayer(String layerId) => void main() { group('Highlighter options', () { test('pen tool options round-trip through JSON', () { - final tool = PenTool(id: 'highlighter', combineHighlights: true); + final tool = PenTool(id: 'highlighter', combinePaths: true); final decoded = Tool.fromJson(tool.toJson()) as PenTool; - expect(decoded.combineHighlights, isTrue); + expect(decoded.combinePaths, isTrue); }); test('pen element combine id round-trips through JSON', () { @@ -32,7 +32,7 @@ void main() { final tool = Tool.fromJson({'type': 'pen'}) as PenTool; final element = PadElement.fromJson({'type': 'pen'}) as PenElement; - expect(tool.combineHighlights, isFalse); + expect(tool.combinePaths, isFalse); expect(element.combineId, isNull); }); }); diff --git a/app/lib/handlers/pen.dart b/app/lib/handlers/pen.dart index 768ad3058562..d68598c751ac 100644 --- a/app/lib/handlers/pen.dart +++ b/app/lib/handlers/pen.dart @@ -188,7 +188,7 @@ class PenHandler extends Handler with ColoredHandler { elements[pointer] = PenElement( id: createUniqueId(), zoom: transform.size, - combineId: data.combineHighlights ? data.id : null, + combineId: data.combinePaths ? data.id : null, collection: state.currentCollection, property: data.property.copyWith( strokeWidth: data.property.strokeWidth / zoom, diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 7610489d6f25..f084edfd8dc5 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -123,7 +123,7 @@ }, "defaultPalette": "Default palette", "highlighter": "Highlighter", - "combineHighlights": "Combine highlights", + "combinePaths": "Combine paths", "add": "Add", "@add": { "description": "Add action" diff --git a/app/lib/selections/tools/pen.dart b/app/lib/selections/tools/pen.dart index 478e05ec21b7..c798ad2c9943 100644 --- a/app/lib/selections/tools/pen.dart +++ b/app/lib/selections/tools/pen.dart @@ -107,12 +107,12 @@ class PenToolSelection extends ToolSelection { ), ), CheckboxListTile( - value: selected.first.combineHighlights, - title: Text(AppLocalizations.of(context).combineHighlights), + value: selected.first.combinePaths, + title: Text(AppLocalizations.of(context).combinePaths), onChanged: (value) => update( context, selected - .map((e) => e.copyWith(combineHighlights: value ?? false)) + .map((e) => e.copyWith(combinePaths: value ?? false)) .toList(), ), ), diff --git a/app/pubspec.lock b/app/pubspec.lock index 01fff8aa9c0f..04cda066a765 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -1726,4 +1726,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.12.2 <4.0.0" - flutter: "3.44.2" + flutter: "3.44.3" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index f5106e53a1ec..87362f70bf51 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -17,7 +17,7 @@ version: 2.6.0-beta.1+187 environment: sdk: ">=3.12.2 <4.0.0" - flutter: 3.44.2 + flutter: 3.44.3 dependencies: flutter: diff --git a/metadata/en-US/changelogs/187.txt b/metadata/en-US/changelogs/187.txt index ca67a83f5802..92d623cd73f4 100644 --- a/metadata/en-US/changelogs/187.txt +++ b/metadata/en-US/changelogs/187.txt @@ -1,3 +1,4 @@ +* Add combine paths option ([#1071](https://github.com/LinwoodDev/Butterfly/issues/1071)) * Add xournal++ exporter * Improve xournal++ importer From 5600b0097508a397bec52a21f0b7fa32255d8193 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Thu, 25 Jun 2026 09:25:14 +0200 Subject: [PATCH 010/117] Fix refresh foregrounds can be run concurrently --- app/lib/cubits/current_index.dart | 7 ++++++- app/pubspec.lock | 4 ++-- metadata/en-US/changelogs/187.txt | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/lib/cubits/current_index.dart b/app/lib/cubits/current_index.dart index 377a4a1c5b50..a366f099b906 100644 --- a/app/lib/cubits/current_index.dart +++ b/app/lib/cubits/current_index.dart @@ -819,7 +819,10 @@ class CurrentIndexCubit extends Cubit { /// Lightweight refresh that only updates foregrounds without rebaking. /// Use this when handler internal state changes but document hasn't changed. - Future refreshForegrounds(DocumentLoaded blocState) async { + Future refreshForegrounds(DocumentLoaded blocState) => + _foregroundRefreshRunner.schedule(() => _refreshForegrounds(blocState)); + + Future _refreshForegrounds(DocumentLoaded blocState) async { if (isClosed) return; final document = blocState.data; final page = blocState.page; @@ -1244,6 +1247,7 @@ class CurrentIndexCubit extends Cubit { final _delayedBakeRunner = CoalescedAsyncRunner( delay: const Duration(milliseconds: 100), ); + final _foregroundRefreshRunner = CoalescedAsyncRunner(delay: Duration.zero); bool _rectContains(Rect outer, Rect inner) { const tolerance = precisionErrorTolerance; @@ -2514,6 +2518,7 @@ class CurrentIndexCubit extends Cubit { _networkingDebounceTimer?.cancel(); _networkingDebounceTimer = null; await _delayedBakeRunner.disposeAndWait(); + await _foregroundRefreshRunner.disposeAndWait(); if (!currentState.networkingService.isClosed) { await currentState.networkingService.close(); } diff --git a/app/pubspec.lock b/app/pubspec.lock index 04cda066a765..252c3a55f13e 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -1041,10 +1041,10 @@ packages: dependency: transitive description: name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_platform_interface: dependency: transitive description: diff --git a/metadata/en-US/changelogs/187.txt b/metadata/en-US/changelogs/187.txt index 92d623cd73f4..6ebdffd9d18e 100644 --- a/metadata/en-US/changelogs/187.txt +++ b/metadata/en-US/changelogs/187.txt @@ -1,5 +1,6 @@ * Add combine paths option ([#1071](https://github.com/LinwoodDev/Butterfly/issues/1071)) * Add xournal++ exporter * Improve xournal++ importer +* Fix refresh foregrounds can be run concurrently Read more here: https://linwood.dev/butterfly/2.6.0-beta.1 \ No newline at end of file From 20db713587f9f4c7b9cb9d560d710f38c24dcb77 Mon Sep 17 00:00:00 2001 From: CodeDoctor Date: Fri, 26 Jun 2026 00:03:02 +0200 Subject: [PATCH 011/117] Upgrade to astro 7 (#1150) --- docs/astro.config.mjs | 30 +- docs/package.json | 15 +- docs/pnpm-lock.yaml | 1743 ++++++++++------- docs/pnpm-workspace.yaml | 4 + docs/src/content/docs/af/community/faq.md | 1 - .../docs/af/community/stylus-support.md | 3 +- .../content/docs/af/docs/v2/tools/label.md | 4 +- docs/src/content/docs/ar/community/faq.md | 1 - .../docs/ar/community/stylus-support.md | 3 +- .../content/docs/ar/docs/v2/tools/label.md | 4 +- docs/src/content/docs/ca/community/faq.md | 1 - .../docs/ca/community/stylus-support.md | 3 +- .../content/docs/ca/docs/v2/tools/label.md | 4 +- docs/src/content/docs/community/faq.md | 1 - .../content/docs/community/stylus-support.md | 3 +- docs/src/content/docs/cs/community/faq.md | 1 - .../docs/cs/community/stylus-support.md | 3 +- .../content/docs/cs/docs/v2/tools/label.md | 4 +- docs/src/content/docs/da/community/faq.md | 1 - .../docs/da/community/stylus-support.md | 3 +- .../content/docs/da/docs/v2/tools/label.md | 4 +- docs/src/content/docs/de/community/faq.md | 1 - .../docs/de/community/stylus-support.md | 3 +- .../content/docs/de/docs/v2/tools/label.md | 4 +- docs/src/content/docs/docs/v2/tools/label.md | 4 +- docs/src/content/docs/el/community/faq.md | 1 - .../docs/el/community/stylus-support.md | 3 +- .../content/docs/el/docs/v2/tools/label.md | 4 +- docs/src/content/docs/es/community/faq.md | 1 - .../docs/es/community/stylus-support.md | 3 +- .../content/docs/es/docs/v2/tools/label.md | 4 +- docs/src/content/docs/fi/community/faq.md | 1 - .../docs/fi/community/stylus-support.md | 3 +- .../content/docs/fi/docs/v2/tools/label.md | 4 +- docs/src/content/docs/fr/community/faq.md | 1 - .../docs/fr/community/stylus-support.md | 3 +- .../content/docs/fr/docs/v2/tools/label.md | 4 +- docs/src/content/docs/he/community/faq.md | 1 - .../docs/he/community/stylus-support.md | 3 +- .../content/docs/he/docs/v2/tools/label.md | 4 +- docs/src/content/docs/hi/community/faq.md | 1 - .../docs/hi/community/stylus-support.md | 3 +- .../content/docs/hi/docs/v2/tools/label.md | 4 +- docs/src/content/docs/hu/community/faq.md | 1 - .../docs/hu/community/stylus-support.md | 3 +- .../content/docs/hu/docs/v2/tools/label.md | 4 +- docs/src/content/docs/id/community/faq.md | 1 - .../docs/id/community/stylus-support.md | 3 +- .../content/docs/id/docs/v2/tools/label.md | 4 +- docs/src/content/docs/it/community/faq.md | 1 - .../docs/it/community/stylus-support.md | 3 +- .../content/docs/it/docs/v2/tools/label.md | 4 +- docs/src/content/docs/ja/community/faq.md | 1 - .../docs/ja/community/stylus-support.md | 3 +- .../content/docs/ja/docs/v2/tools/label.md | 4 +- docs/src/content/docs/ko/community/faq.md | 1 - .../docs/ko/community/stylus-support.md | 3 +- .../content/docs/ko/docs/v2/tools/label.md | 4 +- docs/src/content/docs/nl/community/faq.md | 1 - .../docs/nl/community/stylus-support.md | 3 +- .../content/docs/nl/docs/v2/tools/label.md | 4 +- docs/src/content/docs/no/community/faq.md | 1 - .../docs/no/community/stylus-support.md | 3 +- .../content/docs/no/docs/v2/tools/label.md | 4 +- docs/src/content/docs/or/community/faq.md | 1 - .../docs/or/community/stylus-support.md | 3 +- .../content/docs/or/docs/v2/tools/label.md | 4 +- docs/src/content/docs/pl/community/faq.md | 1 - .../docs/pl/community/stylus-support.md | 3 +- .../content/docs/pl/docs/v2/tools/label.md | 4 +- docs/src/content/docs/pt-br/community/faq.md | 1 - .../docs/pt-br/community/stylus-support.md | 3 +- .../content/docs/pt-br/docs/v2/tools/label.md | 4 +- docs/src/content/docs/pt/community/faq.md | 1 - .../docs/pt/community/stylus-support.md | 3 +- .../content/docs/pt/docs/v2/tools/label.md | 4 +- docs/src/content/docs/ro/community/faq.md | 1 - .../docs/ro/community/stylus-support.md | 3 +- .../content/docs/ro/docs/v2/tools/label.md | 4 +- docs/src/content/docs/ru/community/faq.md | 1 - .../docs/ru/community/stylus-support.md | 3 +- .../content/docs/ru/docs/v2/tools/label.md | 4 +- docs/src/content/docs/sr/community/faq.md | 1 - .../docs/sr/community/stylus-support.md | 3 +- .../content/docs/sr/docs/v2/tools/label.md | 4 +- docs/src/content/docs/sv/community/faq.md | 1 - .../docs/sv/community/stylus-support.md | 3 +- .../content/docs/sv/docs/v2/tools/label.md | 4 +- docs/src/content/docs/th/community/faq.md | 1 - .../docs/th/community/stylus-support.md | 3 +- .../content/docs/th/docs/v2/tools/label.md | 4 +- docs/src/content/docs/tr/community/faq.md | 1 - .../docs/tr/community/stylus-support.md | 3 +- .../content/docs/tr/docs/v2/tools/label.md | 4 +- docs/src/content/docs/uk/community/faq.md | 1 - .../docs/uk/community/stylus-support.md | 3 +- .../content/docs/uk/docs/v2/tools/label.md | 4 +- docs/src/content/docs/vi/community/faq.md | 1 - .../docs/vi/community/stylus-support.md | 3 +- .../content/docs/vi/docs/v2/tools/label.md | 4 +- .../src/content/docs/zh-hant/community/faq.md | 1 - .../docs/zh-hant/community/stylus-support.md | 3 +- .../docs/zh-hant/docs/v2/tools/label.md | 4 +- docs/src/content/docs/zh/community/faq.md | 1 - .../docs/zh/community/stylus-support.md | 3 +- .../content/docs/zh/docs/v2/tools/label.md | 4 +- 106 files changed, 1122 insertions(+), 942 deletions(-) diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 88e9b1b4b731..45a49d5a664b 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -2,20 +2,36 @@ import { defineConfig } from "astro/config"; import starlight from "@astrojs/starlight"; import react from "@astrojs/react"; import { getSidebarTranslatedLabel } from "./src/translations"; -import remarkHeadingID from "remark-heading-id"; -import remarkGemoji from "remark-gemoji"; -import rehypeKatex from "rehype-katex"; import AstroPWA from "@vite-pwa/astro"; import manifest from "./webmanifest.json"; -import remarkMath from "remark-math"; import { fileURLToPath } from "node:url"; +import { satteri } from '@astrojs/markdown-satteri'; +import katex from "katex"; + +const renderMath = (value, displayMode = false) => + katex.renderToString(value, { + displayMode, + throwOnError: false, + }); + +const renderMathPlugin = { + name: "render-math", + inlineMath(node) { + return { rawHtml: renderMath(node.value) }; + }, + math(node) { + return { rawHtml: renderMath(node.value, true) }; + }, +}; // https://astro.build/config export default defineConfig({ site: "https://butterfly.linwood.dev", markdown: { - remarkPlugins: [remarkHeadingID, remarkGemoji, remarkMath], - rehypePlugins: [rehypeKatex] + processor: satteri({ + features: { math: true }, + mdastPlugins: [renderMathPlugin], + }), }, integrations: [ starlight({ @@ -24,7 +40,7 @@ export default defineConfig({ // Relative path to your custom CSS file "./src/styles/linwood-style.scss", "./src/styles/custom.scss", - "node_modules/katex/dist/katex.min.css" + "katex/dist/katex.min.css", ], editLink: { baseUrl: 'https://github.com/LinwoodDev/Butterfly/edit/develop/docs/', diff --git a/docs/package.json b/docs/package.json index 459dc798d39b..3de6ed9fefcd 100644 --- a/docs/package.json +++ b/docs/package.json @@ -11,27 +11,24 @@ }, "dependencies": { "@astrojs/check": "^0.9.9", - "@astrojs/react": "^5.0.7", - "@astrojs/starlight": "^0.40.0", + "@astrojs/markdown-satteri": "^0.3.2", + "@astrojs/react": "^6.0.0", + "@astrojs/starlight": "^0.41.0", "@linwooddev/style": "github:LinwoodDev/style#efbdf8f05d4ef1cf6fec65c90194d30b68b3c64e&path:/packages/web", "@phosphor-icons/react": "^2.1.10", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", - "astro": "^6.4.6", + "astro": "^7.0.2", "katex": "^0.17.0", "react": "^19.2.7", "react-dom": "^19.2.7", - "rehype-katex": "^7.0.1", - "remark-gemoji": "^8.0.0", - "remark-heading-id": "^1.0.1", - "remark-math": "^6.0.0", "typescript": "^6.0.3" }, - "packageManager": "pnpm@11.6.0", + "packageManager": "pnpm@11.9.0", "devDependencies": { "@vite-pwa/astro": "^1.2.0", "sass": "^1.101.0", - "sharp": "^0.35.1", + "sharp": "^0.35.2", "vite-plugin-pwa": "^1.3.0", "workbox-window": "^7.4.1" } diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index 005380464d59..6c04519681ac 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -11,12 +11,15 @@ importers: '@astrojs/check': specifier: ^0.9.9 version: 0.9.9(prettier@3.8.4)(typescript@6.0.3) + '@astrojs/markdown-satteri': + specifier: ^0.3.2 + version: 0.3.2 '@astrojs/react': - specifier: ^5.0.7 - version: 5.0.7(@types/node@24.13.2)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + specifier: ^6.0.0 + version: 6.0.0(@types/node@24.13.2)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) '@astrojs/starlight': - specifier: ^0.40.0 - version: 0.40.0(astro@6.4.6(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3) + specifier: ^0.41.0 + version: 0.41.0(@astrojs/markdown-remark@7.2.0)(astro@7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3) '@linwooddev/style': specifier: github:LinwoodDev/style#efbdf8f05d4ef1cf6fec65c90194d30b68b3c64e&path:/packages/web version: https://codeload.github.com/LinwoodDev/style/tar.gz/efbdf8f05d4ef1cf6fec65c90194d30b68b3c64e#path:/packages/web @@ -30,8 +33,8 @@ importers: specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.17) astro: - specifier: ^6.4.6 - version: 6.4.6(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + specifier: ^7.0.2 + version: 7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) katex: specifier: ^0.17.0 version: 0.17.0 @@ -41,34 +44,22 @@ importers: react-dom: specifier: ^19.2.7 version: 19.2.7(react@19.2.7) - rehype-katex: - specifier: ^7.0.1 - version: 7.0.1 - remark-gemoji: - specifier: ^8.0.0 - version: 8.0.0 - remark-heading-id: - specifier: ^1.0.1 - version: 1.0.1 - remark-math: - specifier: ^6.0.0 - version: 6.0.0 typescript: specifier: ^6.0.3 version: 6.0.3 devDependencies: '@vite-pwa/astro': specifier: ^1.2.0 - version: 1.2.0(astro@6.4.6(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@7.3.5(@types/node@24.13.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1)) + version: 1.2.0(astro@7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1)) sass: specifier: ^1.101.0 version: 1.101.0 sharp: - specifier: ^0.35.1 - version: 0.35.1 + specifier: ^0.35.2 + version: 0.35.2 vite-plugin-pwa: specifier: ^1.3.0 - version: 1.3.0(vite@7.3.5(@types/node@24.13.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1) + version: 1.3.0(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1) workbox-window: specifier: ^7.4.1 version: 7.4.1 @@ -87,12 +78,73 @@ packages: peerDependencies: typescript: ^5.0.0 || ^6.0.0 + '@astrojs/compiler-binding-darwin-arm64@0.2.2': + resolution: {integrity: sha512-1WxpECx3izz5X4Ha3l6ex79HZlUHpKBTElGJsfOosnhVvXhccfcXAsBlrYPNmUOKJWiG7mcrje0ELSr1KPE69Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@astrojs/compiler-binding-darwin-x64@0.2.2': + resolution: {integrity: sha512-PdIQidwQ4nUX/qNL0JXzSwNYadr+yX/Yoo+kZSCxBZqlAqug9SlKR/1H5EG5jSEpkEDRo2d1JdrO1W4kU6uNHw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@astrojs/compiler-binding-linux-arm64-gnu@0.2.2': + resolution: {integrity: sha512-5sZicPkJCoyxTEOW2he+u6UV97pTXY4c7fbHOZ/aslu9ewsunD0231QUBSvnjBB9hRFzzP2JRfNCLY+IvWfw0Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@astrojs/compiler-binding-linux-arm64-musl@0.2.2': + resolution: {integrity: sha512-Vto5fqRzMepQNJPeEhQLOIkmAoNbSBQNlpMe64OHTjEbAtQpJYVHEwe+8WJLaEGLA9qBksLv7ctiYPLLxgVVYQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@astrojs/compiler-binding-linux-x64-gnu@0.2.2': + resolution: {integrity: sha512-NJTTaUDU49WEJT+ImS9evlv3i3x42HN8PbcqdyydI9picnKtuf5TxRL94naoW09YDMYWpkrwVeVqaG7GTSIepQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@astrojs/compiler-binding-linux-x64-musl@0.2.2': + resolution: {integrity: sha512-9nFdNDkWaU35bhWUIciDmi439W0fq2ZNgv1GCi2A/LSb+8vTw9l9z+fVW4YEn7Tlvbs8gyQkJvbc62ZD1H76xA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@astrojs/compiler-binding-wasm32-wasi@0.2.2': + resolution: {integrity: sha512-3NWaxRc3KSwr9zHBEGcQ147rMgAhi/HzZWZJPRN2YLbtuLhoeBPruhdX1MIlOYAg3FvJPYdzGCAKvvWWU6pF0A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@astrojs/compiler-binding-win32-arm64-msvc@0.2.2': + resolution: {integrity: sha512-QsyOgocLGOwDm8zAyuAiziALMzbTTevaqBLv5lvdhyhj2JC5yjp0H3yeEBtE0owRLr1gDQdX0+1qBSc5tTwp4g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@astrojs/compiler-binding-win32-x64-msvc@0.2.2': + resolution: {integrity: sha512-+/C8Oh9YS8C0fwtaqDtUSPniKwlJqgiQbxp7XuzxCP0RI/Jf7yOlz9ZHNr6jD3ZJa4CHdq0mYq7pd8QFiNGIZA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@astrojs/compiler-binding@0.2.2': + resolution: {integrity: sha512-PkCo+UcSxMt9pufjv28kRy8YU88O/XNgm7apwkyhkrCecLY62QCj5UA3nOTMO88PPyVKnUh/6FZbPefBhDD5IA==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@astrojs/compiler-rs@0.2.2': + resolution: {integrity: sha512-C0qoz7Hxa2krlwMYfzQ1ZxOjR6cGmO+iskjRXXCdUM85W/cAPGTi/MSQlLkv0ADMz+Go1/lqeRBjL8AZwsFffQ==} + '@astrojs/compiler@2.13.1': resolution: {integrity: sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg==} - '@astrojs/compiler@4.0.0': - resolution: {integrity: sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA==} - '@astrojs/internal-helpers@0.10.0': resolution: {integrity: sha512-Ry2R3VPeIN4uPCSA4xQc+e+vsJXkalKpEbDc07hV+a/o5Bs2N/s/uDcPJH/05L19DKh9tAy7e6JM3YZ6Cxfezw==} @@ -111,12 +163,15 @@ packages: '@astrojs/markdown-remark@7.2.0': resolution: {integrity: sha512-+YxmVQu1Bd+MFfSzjq1rOJvD9+nIOJzz5YIIhdIH01RrxRkKbyKoEgyIqP3yv51MhzMDgd79QaPv+kCVPT8vHw==} - '@astrojs/mdx@6.0.3': - resolution: {integrity: sha512-+4P3ZvwsRAqAbBgY+uZMewFo3ficlIBPZfu/Luk+v4ia/ZOuFhpsw7r+7672uT2Fc1UPdp7yW0eU5egvSq0wbw==} + '@astrojs/markdown-satteri@0.3.2': + resolution: {integrity: sha512-feXuUPy41gVfeM7EHT1ciUim8ozGr+YHXab9uUBc1Hk8y60DQosO8ldL+AoPXnCAoGj1OChwHfvXmmJ6XVnY9A==} + + '@astrojs/mdx@7.0.0': + resolution: {integrity: sha512-LKwNA8nnLtEM0auoP6OfH/UnlKe1Ub59qZjbcYkZjPBGw6PkJewWkA/1qwLpECvV6gMDd6TR6eqV9p/VYZrcrQ==} engines: {node: '>=22.12.0'} peerDependencies: - '@astrojs/markdown-satteri': 0.3.0 - astro: ^6.4.0 + '@astrojs/markdown-satteri': ^0.3.1-alpha.0 + astro: ^7.0.0-alpha.0 peerDependenciesMeta: '@astrojs/markdown-satteri': optional: true @@ -125,8 +180,8 @@ packages: resolution: {integrity: sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==} engines: {node: '>=22.12.0'} - '@astrojs/react@5.0.7': - resolution: {integrity: sha512-N9cCoxvnLWaP+AK1Fv4e5Mc7ktnVTpSo2nWLwvD9Ohr1dJKygwrTSm9yatqoahgb1A5Kwjg/rT2shRiIVdn3aw==} + '@astrojs/react@6.0.0': + resolution: {integrity: sha512-jNf3kKE6KYXJbD5ZsXaLhnwDK3YvK9ttQ2ykAcNf5XdxOlUQEHLnomO3FO8abqghs0ilqhgGOyc2AlgGyQtz/g==} engines: {node: '>=22.12.0'} peerDependencies: '@types/react': ^17.0.50 || ^18.0.21 || ^19.0.0 @@ -137,13 +192,13 @@ packages: '@astrojs/sitemap@3.7.3': resolution: {integrity: sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA==} - '@astrojs/starlight@0.40.0': - resolution: {integrity: sha512-H1NBIXx4Xw6YzKMsoMkazYxFgnTTj6pD4IReUGWj1fqw82AOAgj+WnZLpTDWRExf3b9ZM7Popbl583i4IvDNVQ==} + '@astrojs/starlight@0.41.0': + resolution: {integrity: sha512-5QQLMArxdnLOQhKbXTEU4wh4hydUilXHKG6llOfIZuggM3Mr4b6RvdvInYtqWLfeAZxnlvIHGrwurC3wWW9zRg==} peerDependencies: - '@astrojs/markdown-satteri': ^0.2.0 - astro: ^6.4.5 + '@astrojs/markdown-remark': ^7.2.0 + astro: ^7.0.2 peerDependenciesMeta: - '@astrojs/markdown-satteri': + '@astrojs/markdown-remark': optional: true '@astrojs/telemetry@3.3.2': @@ -666,16 +721,42 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@bruits/satteri-darwin-arm64@0.9.1': + resolution: {integrity: sha512-NE4qC2sRd0+R+oPMsKcUikIvWTGpAV16fSXNBoMSW7nK7Pd9e/ZGB7M8knep83pFmGmOb/5NbGKiVIh3oLbljw==} + cpu: [arm64] + os: [darwin] + + '@bruits/satteri-darwin-x64@0.9.1': + resolution: {integrity: sha512-Am8z5nX0L/sJR/n7np+5rMVP434MOBe3zlU+IO8fXbv2UaPNRCrEQPMS6lyxCj7dZHQI2AynSJw3v4fgQE8xSQ==} + cpu: [x64] + os: [darwin] + + '@bruits/satteri-linux-x64-gnu@0.9.1': + resolution: {integrity: sha512-Bivw60+SIfmlaU9wEZ39HAcyPh1xU9TvOrz4KJgc+ziRStQs4EzYoWW4Eh9aSdiol+xV0vjEWYuA79aF3dr7kg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@bruits/satteri-wasm32-wasi@0.9.1': + resolution: {integrity: sha512-lEFk5ebh5SxRquA5RJisEoMoDmOiSnS10PGgXgXeVlWgF8KWKI9D3hSzVqNjEg/qKOjHcNdjIfyx/03Wrl6J3g==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@bruits/satteri-win32-x64-msvc@0.9.1': + resolution: {integrity: sha512-YBhm8yARopswAPeTih11BzMxWVQFD5CI9Yryp6HWepi6F/CeMycqxv18DXrj9U2fDDlbVGwT2IiEM2UPBjIPDQ==} + cpu: [x64] + os: [win32] + '@capsizecss/unpack@4.0.1': resolution: {integrity: sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==} engines: {node: '>=18'} - '@clack/core@1.4.1': - resolution: {integrity: sha512-FILJa1gGKEFTGZAJE9RpVhrjKz3c3h4ar60dSv6cGuDqufQ84YEIS3GAGvZiN+H6yaLbbvTFNejjCC4tXpZEuw==} + '@clack/core@1.4.2': + resolution: {integrity: sha512-0Ty/1Gfm+Kb07sXcuESjyKfwEhSy4Ns1AgeEisHb/bDY5fWme0tTeTkU14T1Gmcs17YIjB/teiDe4uaCghbYqQ==} engines: {node: '>= 20.12.0'} - '@clack/prompts@1.5.1': - resolution: {integrity: sha512-zccHj2z2oCCO4yrDiRSlFOxWerGqRiysP7a5jPK6uoI9URKAquwY42Dd/iUP8JWHxEzdRe4TlbvZCo8z1/mhrw==} + '@clack/prompts@1.6.0': + resolution: {integrity: sha512-EYlRokl8szrP9Z25qT5aepMdBjzBvHF9ZEhzIiUBc9guz/T31EqRgvD0QSgZcpE93xiwrr+OkB4nz0BZyF6fSA==} engines: {node: '>= 20.12.0'} '@ctrl/tinycolor@4.2.0': @@ -703,161 +784,179 @@ packages: '@emmetio/stream-reader@2.2.0': resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/core@1.9.1': + resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + '@emnapi/runtime@1.9.1': + resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} + + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -884,8 +983,8 @@ packages: cpu: [arm64] os: [darwin] - '@img/sharp-darwin-arm64@0.35.1': - resolution: {integrity: sha512-T15JRWOubQ3f5+GxnWeIvo47u5qV0M9HBgJhT+f2gE1e9e6OhR6K73Re52Hm80qWcu1DNb3GweKmpr/MnuP2Ow==} + '@img/sharp-darwin-arm64@0.35.2': + resolution: {integrity: sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] @@ -896,14 +995,14 @@ packages: cpu: [x64] os: [darwin] - '@img/sharp-darwin-x64@0.35.1': - resolution: {integrity: sha512-t1CPD0cr7XCHjwUj6tQ5MC0pCi866I+gUW6zbUX4aFPnKd1DFBtk0M+gWcjX8VeEzgfCNiSiNTVFZ6b7kvdbnQ==} + '@img/sharp-darwin-x64@0.35.2': + resolution: {integrity: sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==} engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-freebsd-wasm32@0.35.1': - resolution: {integrity: sha512-MBSQXqNPThW9EcZ905H6N4sEdX5EwZEYzGx5EBq9ncDCGJALMiY1xPFJxNdzuB1iBjLOpIfxajM6YxdvwmQSLA==} + '@img/sharp-freebsd-wasm32@0.35.2': + resolution: {integrity: sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==} engines: {node: '>=20.9.0'} os: [freebsd] @@ -912,8 +1011,8 @@ packages: cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.3.0': - resolution: {integrity: sha512-EKbmBKtyTH+GPFDRw2TgK2oV6hyxxlJVIar4hoTYSNmIwipgMFdxPQqR392GmfdsPGWga0mCFN1cCKjRb9cljw==} + '@img/sharp-libvips-darwin-arm64@1.3.1': + resolution: {integrity: sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==} cpu: [arm64] os: [darwin] @@ -922,8 +1021,8 @@ packages: cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.0': - resolution: {integrity: sha512-Pl2OmOvrJ42adUllESxBsG54PfXLo1OYg9i3c5/5Ln/qJ0gZuTM9YMhQJPIbXqwidLRc/c2zuHt4RsrymmNv7A==} + '@img/sharp-libvips-darwin-x64@1.3.1': + resolution: {integrity: sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==} cpu: [x64] os: [darwin] @@ -933,8 +1032,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm64@1.3.0': - resolution: {integrity: sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ==} + '@img/sharp-libvips-linux-arm64@1.3.1': + resolution: {integrity: sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==} cpu: [arm64] os: [linux] libc: [glibc] @@ -945,8 +1044,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.3.0': - resolution: {integrity: sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A==} + '@img/sharp-libvips-linux-arm@1.3.1': + resolution: {integrity: sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==} cpu: [arm] os: [linux] libc: [glibc] @@ -957,8 +1056,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.3.0': - resolution: {integrity: sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A==} + '@img/sharp-libvips-linux-ppc64@1.3.1': + resolution: {integrity: sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==} cpu: [ppc64] os: [linux] libc: [glibc] @@ -969,8 +1068,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.3.0': - resolution: {integrity: sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA==} + '@img/sharp-libvips-linux-riscv64@1.3.1': + resolution: {integrity: sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==} cpu: [riscv64] os: [linux] libc: [glibc] @@ -981,8 +1080,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.3.0': - resolution: {integrity: sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA==} + '@img/sharp-libvips-linux-s390x@1.3.1': + resolution: {integrity: sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==} cpu: [s390x] os: [linux] libc: [glibc] @@ -993,8 +1092,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.3.0': - resolution: {integrity: sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA==} + '@img/sharp-libvips-linux-x64@1.3.1': + resolution: {integrity: sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==} cpu: [x64] os: [linux] libc: [glibc] @@ -1005,8 +1104,8 @@ packages: os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-arm64@1.3.0': - resolution: {integrity: sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': + resolution: {integrity: sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==} cpu: [arm64] os: [linux] libc: [musl] @@ -1017,8 +1116,8 @@ packages: os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.3.0': - resolution: {integrity: sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw==} + '@img/sharp-libvips-linuxmusl-x64@1.3.1': + resolution: {integrity: sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==} cpu: [x64] os: [linux] libc: [musl] @@ -1030,8 +1129,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-arm64@0.35.1': - resolution: {integrity: sha512-ErCRyGU7LeoaFBZ0xW8hhLlXzhAg80sc4vxePB86qvtEvW1jEhhmbiNBP4oEzZfPMnu6HwHXfzD2W2kBU+RnCw==} + '@img/sharp-linux-arm64@0.35.2': + resolution: {integrity: sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] @@ -1044,8 +1143,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.35.1': - resolution: {integrity: sha512-jygmR02PpCYypt7xB7nst1vqjZp/BpRA/Kf9nK7qRponJ/KrLPaZWEG4G15z1d2FZ6XqI+T0350ha3RSnKx24A==} + '@img/sharp-linux-arm@0.35.2': + resolution: {integrity: sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] @@ -1058,8 +1157,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.35.1': - resolution: {integrity: sha512-LUWZ2+r2UoLCd8j0RLCwQ4gL6w47+Y7igxtVnPIDXOOEjV86LpBkAHq5VpJeg+GHbw0KN/JWlPJOdZjyZnFqFQ==} + '@img/sharp-linux-ppc64@0.35.2': + resolution: {integrity: sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] @@ -1072,8 +1171,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.35.1': - resolution: {integrity: sha512-i7x6J3mwF4JgT0sM4V4WlAWdJ0bucPtA9rzO1bTji1n5qgBq/W5nn87RvOQPleuuxahNoLdTngByD8/vDDLArw==} + '@img/sharp-linux-riscv64@0.35.2': + resolution: {integrity: sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] @@ -1086,8 +1185,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.35.1': - resolution: {integrity: sha512-0zSaTUjTF0kIWTSYxD4EG/nvCU4jez53+3RdURtoY3HvbXtIQ98W90JnrGz/oLRFuEnfIy9+7xeq883euc0ZWw==} + '@img/sharp-linux-s390x@0.35.2': + resolution: {integrity: sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] @@ -1100,8 +1199,8 @@ packages: os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.35.1': - resolution: {integrity: sha512-NbJD4mWdeyrNQKluO/tR/wBDOelcowSVGNBWxI0e3ZtlXc6F/UOVKDj1MLD4zl3oHTuvKW3s+MA9N54YTldAYw==} + '@img/sharp-linux-x64@0.35.2': + resolution: {integrity: sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] @@ -1114,8 +1213,8 @@ packages: os: [linux] libc: [musl] - '@img/sharp-linuxmusl-arm64@0.35.1': - resolution: {integrity: sha512-VoW2sQCWI+0YIKQEmWJ8vzaQjTg9wIyfkFpvEfAS2h43X6iHu7GTk1hhOgB4IpSzCHe8UwQZIcx7b81VTaOrJA==} + '@img/sharp-linuxmusl-arm64@0.35.2': + resolution: {integrity: sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] @@ -1128,8 +1227,8 @@ packages: os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.35.1': - resolution: {integrity: sha512-LjBoSd/c5JU0/K5MwzDMlgsSRP2bPn98JQGFFQAOLQ0bU/1z4ekxUdSKY9BmlwSh/cA+OrvpgsWqfZyYfVHBRw==} + '@img/sharp-linuxmusl-x64@0.35.2': + resolution: {integrity: sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] @@ -1140,12 +1239,12 @@ packages: engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-wasm32@0.35.1': - resolution: {integrity: sha512-PCQUoQdZyE8tp3HpbevuihfUmgSP4qWI0FGEPWoeXqaS+cUrFfemabHQiebUmUmlUhCuNnQMxGrQ+CPqK4hnxg==} + '@img/sharp-wasm32@0.35.2': + resolution: {integrity: sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==} engines: {node: '>=20.9.0'} - '@img/sharp-webcontainers-wasm32@0.35.1': - resolution: {integrity: sha512-xU2ml2bU2OPxYVvW2A6ae4M1g5QKyhKG06P4FAt+YEaFQQO0919Qx+XxIZEUuWTMoDViLpMws2/dQwoe/VcA6A==} + '@img/sharp-webcontainers-wasm32@0.35.2': + resolution: {integrity: sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==} engines: {node: '>=20.9.0'} cpu: [wasm32] @@ -1155,8 +1254,8 @@ packages: cpu: [arm64] os: [win32] - '@img/sharp-win32-arm64@0.35.1': - resolution: {integrity: sha512-IkmHwuFhYpd3bTsN5SAahjwhiAcyXPooBt8vEUgxY3T0IP70sSJ0nU1xiPzZY8AH/OB1XpV3j8aZSVSOSfTbdA==} + '@img/sharp-win32-arm64@0.35.2': + resolution: {integrity: sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] @@ -1167,8 +1266,8 @@ packages: cpu: [ia32] os: [win32] - '@img/sharp-win32-ia32@0.35.1': - resolution: {integrity: sha512-wQahqCi9MD8Yxzg4gVM4fNrZxh+r6vD55PyIg+WJPaM5ZRUyF35iQpwJCuma3r6viU9/8Pxlc+XHV+woVa6nCQ==} + '@img/sharp-win32-ia32@0.35.2': + resolution: {integrity: sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==} engines: {node: ^20.9.0} cpu: [ia32] os: [win32] @@ -1179,8 +1278,8 @@ packages: cpu: [x64] os: [win32] - '@img/sharp-win32-x64@0.35.1': - resolution: {integrity: sha512-WzBtkYtZHATLPe8XRharxZXxQ9cdLrQWHiwxt+BJ5rBsisQrKeeV86ErxPSVhcG6xCEuNhs0SqLpWr7XDa2k6w==} + '@img/sharp-win32-x64@0.35.2': + resolution: {integrity: sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==} engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -1211,9 +1310,18 @@ packages: '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} + '@napi-rs/wasm-runtime@1.1.5': + resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@pagefind/darwin-arm64@1.5.2': resolution: {integrity: sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==} cpu: [arm64] @@ -1347,9 +1455,107 @@ packages: react: '>= 16.8' react-dom: '>= 16.8' + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rolldown/pluginutils@1.0.0-rc.3': resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/plugin-babel@5.3.1': resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} engines: {node: '>= 10.0.0'} @@ -1399,144 +1605,6 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.61.1': - resolution: {integrity: sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.61.1': - resolution: {integrity: sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.61.1': - resolution: {integrity: sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.61.1': - resolution: {integrity: sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.61.1': - resolution: {integrity: sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.61.1': - resolution: {integrity: sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.61.1': - resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.61.1': - resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.61.1': - resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm64-musl@4.61.1': - resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-loong64-gnu@4.61.1': - resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.61.1': - resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.61.1': - resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-musl@4.61.1': - resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.61.1': - resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.61.1': - resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.61.1': - resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-gnu@4.61.1': - resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-musl@4.61.1': - resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rollup/rollup-openbsd-x64@4.61.1': - resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.61.1': - resolution: {integrity: sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.61.1': - resolution: {integrity: sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.61.1': - resolution: {integrity: sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.61.1': - resolution: {integrity: sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.61.1': - resolution: {integrity: sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==} - cpu: [x64] - os: [win32] - '@shikijs/core@4.2.0': resolution: {integrity: sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ==} engines: {node: '>=20'} @@ -1571,6 +1639,9 @@ packages: '@surma/rollup-plugin-off-main-thread@2.2.3': resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1601,9 +1672,6 @@ packages: '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} - '@types/katex@0.16.8': - resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} - '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -1642,8 +1710,8 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@ungap/structured-clone@1.3.1': - resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + '@ungap/structured-clone@1.3.2': + resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==} '@vite-pwa/astro@1.2.0': resolution: {integrity: sha512-ZJYkc87j/nuuBJjZEYe0C4MeXH3UHcYPWuSH0qq3dRIhlm8ej4ygPZm9YyB6rWNuPZ7ixRX+hVWHU3NaXfkzyA==} @@ -1708,6 +1776,10 @@ packages: ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + am-i-vibing@0.4.0: + resolution: {integrity: sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg==} + hasBin: true + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1750,10 +1822,15 @@ packages: peerDependencies: astro: ^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta - astro@6.4.6: - resolution: {integrity: sha512-48OBTBKR9ctbf+DQxpOuxGl8ebfn59zTuNQMBzptmG/Mi/H8IdfMSbJgGuX1I/4U6g9yazG1p4BHlf4+2hWU4Q==} + astro@7.0.2: + resolution: {integrity: sha512-Rj31HS85pVSfiQTxKTwH6vTmF8y57iRfkgb1uiCTtdLIh3jlUKPAPXtEmHXRKp/PdTS00D4EXYlWXypY+AVVCA==} engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true + peerDependencies: + '@astrojs/markdown-remark': 7.2.0 + peerDependenciesMeta: + '@astrojs/markdown-remark': + optional: true async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} @@ -1795,8 +1872,8 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.10.37: - resolution: {integrity: sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==} + baseline-browser-mapping@2.10.38: + resolution: {integrity: sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==} engines: {node: '>=6.0.0'} hasBin: true @@ -1815,8 +1892,8 @@ packages: brace-expansion@2.1.1: resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + browserslist@4.28.4: + resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2048,8 +2125,8 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.5.372: - resolution: {integrity: sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==} + electron-to-chromium@1.5.377: + resolution: {integrity: sha512-cH1jZgJHoezfTnKfKwnScpHywTFVnJUNITDPREFdhNjiuD502+QFpG0Qk7G8jhsV/f+CEAFlIrzP1fT+IMb92g==} emmet@2.4.11: resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} @@ -2065,6 +2142,10 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + es-abstract-get@1.0.0: + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} + es-abstract@1.24.2: resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} engines: {node: '>= 0.4'} @@ -2088,8 +2169,8 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - es-to-primitive@1.3.0: - resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + es-to-primitive@1.3.1: + resolution: {integrity: sha512-CxN9N56HYfd2m/acc/NOFrZQsN9kU4eh+2kk6A707Kz1krH8tKmfrs5RnftB8WNX80T0NS7vSQsDOlg23diR2g==} engines: {node: '>= 0.4'} esast-util-from-estree@2.0.0: @@ -2098,8 +2179,8 @@ packages: esast-util-from-js@2.0.1: resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true @@ -2218,9 +2299,6 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - gemoji@8.1.0: - resolution: {integrity: sha512-HA4Gx59dw2+tn+UAa7XEV4ufUKI4fH1KgcbenVA9YKSj1QJTT0xh5Mwv5HMFNN3l2OtUe3ZIfuRwSyZS5pLIWw==} - generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -2302,12 +2380,6 @@ packages: hast-util-format@1.1.0: resolution: {integrity: sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA==} - hast-util-from-dom@5.0.1: - resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==} - - hast-util-from-html-isomorphic@2.0.0: - resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==} - hast-util-from-html@2.0.3: resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} @@ -2385,8 +2457,8 @@ packages: idb@7.1.1: resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} - immutable@5.1.6: - resolution: {integrity: sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==} + immutable@5.1.7: + resolution: {integrity: sha512-47Xb+LFbZ/ZIjQMj6Q5J3IfK7PJFuqRdFOC9FpGgRTK6U2dAEVmkR9hp58qU4FpYux5YXpneDwkj2EP6lppzFA==} inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} @@ -2600,10 +2672,6 @@ packages: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} - katex@0.16.47: - resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} - hasBin: true - katex@0.17.0: resolution: {integrity: sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==} hasBin: true @@ -2620,6 +2688,80 @@ packages: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} @@ -2689,9 +2831,6 @@ packages: mdast-util-gfm@3.1.0: resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} - mdast-util-math@3.0.0: - resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==} - mdast-util-mdx-expression@2.0.1: resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} @@ -2749,9 +2888,6 @@ packages: micromark-extension-gfm@3.0.0: resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} - micromark-extension-math@3.1.0: - resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} - micromark-extension-mdx-expression@3.0.1: resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} @@ -2850,8 +2986,8 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} - nanoid@3.3.12: - resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -2871,8 +3007,8 @@ packages: node-mock-http@1.0.4: resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} - node-releases@2.0.47: - resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==} + node-releases@2.0.48: + resolution: {integrity: sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==} engines: {node: '>=18'} normalize-path@3.0.0: @@ -3004,6 +3140,10 @@ packages: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} + process-ancestry@0.1.0: + resolution: {integrity: sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg==} + engines: {node: '>=18.0.0'} + property-information@7.2.0: resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} @@ -3083,8 +3223,8 @@ packages: regjsgen@0.8.0: resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} - regjsparser@0.13.1: - resolution: {integrity: sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==} + regjsparser@0.13.2: + resolution: {integrity: sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==} hasBin: true rehype-expressive-code@0.43.1: @@ -3093,9 +3233,6 @@ packages: rehype-format@5.0.1: resolution: {integrity: sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ==} - rehype-katex@7.0.1: - resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} - rehype-parse@9.0.1: resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} @@ -3114,19 +3251,9 @@ packages: remark-directive@4.0.0: resolution: {integrity: sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==} - remark-gemoji@8.0.0: - resolution: {integrity: sha512-/fL9rc72FYwFGtOKcT+QeQdx9Q9t5v4N6KLXSDOTEgaedzK85I9judBqB2eqz+g4b0ERMejlwSOuPK+wket6aA==} - remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} - remark-heading-id@1.0.1: - resolution: {integrity: sha512-GmJjuCeEkYvwFlvn/Skjc/1Qafj71412gbQnrwUmP/tKskmAf1cMRlZRNoovV+aIvsSRkTb2rCmGv2b9RdoJbQ==} - engines: {node: '>=8'} - - remark-math@6.0.0: - resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==} - remark-mdx@3.1.1: resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==} @@ -3177,16 +3304,16 @@ packages: retext@9.0.0: resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup@2.80.0: resolution: {integrity: sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==} engines: {node: '>=10.0.0'} hasBin: true - rollup@4.61.1: - resolution: {integrity: sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - safe-array-concat@1.1.4: resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} engines: {node: '>=0.4'} @@ -3207,6 +3334,9 @@ packages: engines: {node: '>=20.19.0'} hasBin: true + satteri@0.9.1: + resolution: {integrity: sha512-0oIBjwDxWvz8ePRSBxv8vEdZ0GI25/UcSq11y4651tJztUAmZlIdPjFx8luqBAM22g8YKVr5R17KaaZ2kIfLrw==} + sax@1.6.0: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} @@ -3218,8 +3348,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.8.4: - resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true @@ -3242,8 +3372,8 @@ packages: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - sharp@0.35.1: - resolution: {integrity: sha512-lW979AMi+ESidzMv/Lnv+F9bknzLyxLqFI05Sm433vOeRcltgxQmXpnfOOFIAlKtwXU/ksupm2srQoFCkR214g==} + sharp@0.35.2: + resolution: {integrity: sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==} engines: {node: '>=20.9.0'} shiki@4.2.0: @@ -3278,8 +3408,8 @@ packages: resolution: {integrity: sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==} engines: {node: '>=20.0.0'} - smol-toml@1.6.1: - resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + smol-toml@1.7.0: + resolution: {integrity: sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==} engines: {node: '>= 18'} source-map-js@1.2.1: @@ -3382,8 +3512,8 @@ packages: tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} - tinyclip@0.1.14: - resolution: {integrity: sha512-F1oWdz8tjT17qe1d5JgDK6z03WGOhYYAN0lK3/D/fzNiy93xswLLEw7pk+3g05onhAy6Bsc6PLNUGhdgVjemMQ==} + tinyclip@0.1.15: + resolution: {integrity: sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==} engines: {node: ^16.14.0 || >= 17.3.0} tinyexec@1.2.4: @@ -3482,9 +3612,6 @@ packages: unist-util-find-after@5.0.0: resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} - unist-util-is@3.0.0: - resolution: {integrity: sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==} - unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -3506,15 +3633,9 @@ packages: unist-util-visit-children@3.0.0: resolution: {integrity: sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==} - unist-util-visit-parents@2.1.2: - resolution: {integrity: sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==} - unist-util-visit-parents@6.0.2: resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} - unist-util-visit@1.4.1: - resolution: {integrity: sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==} - unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} @@ -3618,15 +3739,16 @@ packages: '@vite-pwa/assets-generator': optional: true - vite@7.3.5: - resolution: {integrity: sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==} + vite@8.0.16: + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 - lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 stylus: '>=0.54.8' @@ -3637,12 +3759,14 @@ packages: peerDependenciesMeta: '@types/node': optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true jiti: optional: true less: optional: true - lightningcss: - optional: true sass: optional: true sass-embedded: @@ -3746,8 +3870,8 @@ packages: vscode-languageserver-protocol@3.17.5: resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} - vscode-languageserver-protocol@3.18.0: - resolution: {integrity: sha512-Zdz+kJ12Iz6tc11xfZyEo501bBATHXrCjmMfnaR3pMnf1CoqZBKIynba3P+/bi9VEdrMbNtAVKYpKhbODvqy+Q==} + vscode-languageserver-protocol@3.18.1: + resolution: {integrity: sha512-RTiiVHdpxpYcJVI5sq6S5TLjQ4WDR/rBrIWru+kPXe6sGQ9PFQ3GamrTKLvPqbR4ylr1SoodhmcqbFII0WXVuw==} vscode-languageserver-textdocument@1.0.12: resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} @@ -3891,8 +4015,8 @@ packages: resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} engines: {node: '>=12'} yocto-queue@1.2.2: @@ -3919,15 +4043,67 @@ snapshots: chokidar: 4.0.3 kleur: 4.1.5 typescript: 6.0.3 - yargs: 17.7.2 + yargs: 17.7.3 + transitivePeerDependencies: + - prettier + - prettier-plugin-astro + + '@astrojs/compiler-binding-darwin-arm64@0.2.2': + optional: true + + '@astrojs/compiler-binding-darwin-x64@0.2.2': + optional: true + + '@astrojs/compiler-binding-linux-arm64-gnu@0.2.2': + optional: true + + '@astrojs/compiler-binding-linux-arm64-musl@0.2.2': + optional: true + + '@astrojs/compiler-binding-linux-x64-gnu@0.2.2': + optional: true + + '@astrojs/compiler-binding-linux-x64-musl@0.2.2': + optional: true + + '@astrojs/compiler-binding-wasm32-wasi@0.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@astrojs/compiler-binding-win32-arm64-msvc@0.2.2': + optional: true + + '@astrojs/compiler-binding-win32-x64-msvc@0.2.2': + optional: true + + '@astrojs/compiler-binding@0.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)': + optionalDependencies: + '@astrojs/compiler-binding-darwin-arm64': 0.2.2 + '@astrojs/compiler-binding-darwin-x64': 0.2.2 + '@astrojs/compiler-binding-linux-arm64-gnu': 0.2.2 + '@astrojs/compiler-binding-linux-arm64-musl': 0.2.2 + '@astrojs/compiler-binding-linux-x64-gnu': 0.2.2 + '@astrojs/compiler-binding-linux-x64-musl': 0.2.2 + '@astrojs/compiler-binding-wasm32-wasi': 0.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1) + '@astrojs/compiler-binding-win32-arm64-msvc': 0.2.2 + '@astrojs/compiler-binding-win32-x64-msvc': 0.2.2 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + '@astrojs/compiler-rs@0.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)': + dependencies: + '@astrojs/compiler-binding': 0.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1) transitivePeerDependencies: - - prettier - - prettier-plugin-astro + - '@emnapi/core' + - '@emnapi/runtime' '@astrojs/compiler@2.13.1': {} - '@astrojs/compiler@4.0.0': {} - '@astrojs/internal-helpers@0.10.0': dependencies: '@types/hast': 3.0.4 @@ -3936,7 +4112,7 @@ snapshots: picomatch: 4.0.4 retext-smartypants: 6.2.0 shiki: 4.2.0 - smol-toml: 1.6.1 + smol-toml: 1.7.0 unified: 11.0.5 '@astrojs/language-server@2.16.10(prettier@3.8.4)(typescript@6.0.3)': @@ -3986,13 +4162,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/mdx@6.0.3(astro@6.4.6(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': + '@astrojs/markdown-satteri@0.3.2': + dependencies: + '@astrojs/internal-helpers': 0.10.0 + '@astrojs/prism': 4.0.2 + github-slugger: 2.0.0 + satteri: 0.9.1 + + '@astrojs/mdx@7.0.0(@astrojs/markdown-satteri@0.3.2)(astro@7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: '@astrojs/internal-helpers': 0.10.0 '@astrojs/markdown-remark': 7.2.0 '@mdx-js/mdx': 3.1.1 acorn: 8.17.0 - astro: 6.4.6(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + astro: 7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) es-module-lexer: 2.1.0 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 @@ -4003,6 +4186,8 @@ snapshots: source-map: 0.7.6 unist-util-visit: 5.1.0 vfile: 6.0.3 + optionalDependencies: + '@astrojs/markdown-satteri': 0.3.2 transitivePeerDependencies: - supports-color @@ -4010,22 +4195,23 @@ snapshots: dependencies: prismjs: 1.30.0 - '@astrojs/react@5.0.7(@types/node@24.13.2)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)': + '@astrojs/react@6.0.0(@types/node@24.13.2)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)': dependencies: '@astrojs/internal-helpers': 0.10.0 '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@vitejs/plugin-react': 5.2.0(vite@7.3.5(@types/node@24.13.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + '@vitejs/plugin-react': 5.2.0(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) devalue: 5.8.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) ultrahtml: 1.6.0 - vite: 7.3.5(@types/node@24.13.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' + - '@vitejs/devtools' + - esbuild - jiti - less - - lightningcss - sass - sass-embedded - stylus @@ -4041,17 +4227,17 @@ snapshots: stream-replace-string: 2.0.0 zod: 4.4.3 - '@astrojs/starlight@0.40.0(astro@6.4.6(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3)': + '@astrojs/starlight@0.41.0(@astrojs/markdown-remark@7.2.0)(astro@7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3)': dependencies: - '@astrojs/markdown-remark': 7.2.0 - '@astrojs/mdx': 6.0.3(astro@6.4.6(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + '@astrojs/markdown-satteri': 0.3.2 + '@astrojs/mdx': 7.0.0(@astrojs/markdown-satteri@0.3.2)(astro@7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) '@astrojs/sitemap': 3.7.3 '@pagefind/default-ui': 1.5.2 '@types/hast': 3.0.4 '@types/js-yaml': 4.0.9 '@types/mdast': 4.0.4 - astro: 6.4.6(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - astro-expressive-code: 0.43.1(astro@6.4.6(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + astro: 7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + astro-expressive-code: 0.43.1(astro@7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) bcp-47: 2.1.0 hast-util-from-html: 2.0.3 hast-util-select: 6.0.4 @@ -4068,10 +4254,13 @@ snapshots: rehype: 13.0.2 rehype-format: 5.0.1 remark-directive: 4.0.0 + satteri: 0.9.1 ultrahtml: 1.6.0 unified: 11.0.5 unist-util-visit: 5.1.0 vfile: 6.0.3 + optionalDependencies: + '@astrojs/markdown-remark': 7.2.0 transitivePeerDependencies: - supports-color - typescript @@ -4132,7 +4321,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.2 + browserslist: 4.28.4 lru-cache: 5.1.1 semver: 6.3.1 @@ -4761,18 +4950,37 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@bruits/satteri-darwin-arm64@0.9.1': + optional: true + + '@bruits/satteri-darwin-x64@0.9.1': + optional: true + + '@bruits/satteri-linux-x64-gnu@0.9.1': + optional: true + + '@bruits/satteri-wasm32-wasi@0.9.1': + dependencies: + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + optional: true + + '@bruits/satteri-win32-x64-msvc@0.9.1': + optional: true + '@capsizecss/unpack@4.0.1': dependencies: fontkitten: 1.0.3 - '@clack/core@1.4.1': + '@clack/core@1.4.2': dependencies: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 - '@clack/prompts@1.5.1': + '@clack/prompts@1.6.0': dependencies: - '@clack/core': 1.4.1 + '@clack/core': 1.4.2 fast-string-width: 3.0.2 fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 @@ -4802,87 +5010,119 @@ snapshots: '@emmetio/stream-reader@2.2.0': {} + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/core@1.9.1': + dependencies: + '@emnapi/wasi-threads': 1.2.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.27.7': + '@emnapi/runtime@1.9.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm64@0.27.7': + '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/android-arm@0.27.7': + '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/android-x64@0.27.7': + '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.27.7': + '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/darwin-x64@0.27.7': + '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.27.7': + '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/freebsd-x64@0.27.7': + '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.27.7': + '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/linux-arm@0.27.7': + '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/linux-ia32@0.27.7': + '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/linux-loong64@0.27.7': + '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.27.7': + '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/linux-ppc64@0.27.7': + '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.27.7': + '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/linux-s390x@0.27.7': + '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/linux-x64@0.27.7': + '@esbuild/linux-x64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.27.7': + '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.27.7': + '@esbuild/netbsd-x64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.27.7': + '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.27.7': + '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.27.7': + '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/sunos-x64@0.27.7': + '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/win32-arm64@0.27.7': + '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/win32-ia32@0.27.7': + '@esbuild/win32-ia32@0.28.1': optional: true - '@esbuild/win32-x64@0.27.7': + '@esbuild/win32-x64@0.28.1': optional: true '@expressive-code/core@0.43.1': @@ -4917,9 +5157,9 @@ snapshots: '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true - '@img/sharp-darwin-arm64@0.35.1': + '@img/sharp-darwin-arm64@0.35.2': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.3.0 + '@img/sharp-libvips-darwin-arm64': 1.3.1 optional: true '@img/sharp-darwin-x64@0.34.5': @@ -4927,74 +5167,74 @@ snapshots: '@img/sharp-libvips-darwin-x64': 1.2.4 optional: true - '@img/sharp-darwin-x64@0.35.1': + '@img/sharp-darwin-x64@0.35.2': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.3.0 + '@img/sharp-libvips-darwin-x64': 1.3.1 optional: true - '@img/sharp-freebsd-wasm32@0.35.1': + '@img/sharp-freebsd-wasm32@0.35.2': dependencies: - '@img/sharp-wasm32': 0.35.1 + '@img/sharp-wasm32': 0.35.2 optional: true '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true - '@img/sharp-libvips-darwin-arm64@1.3.0': + '@img/sharp-libvips-darwin-arm64@1.3.1': optional: true '@img/sharp-libvips-darwin-x64@1.2.4': optional: true - '@img/sharp-libvips-darwin-x64@1.3.0': + '@img/sharp-libvips-darwin-x64@1.3.1': optional: true '@img/sharp-libvips-linux-arm64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm64@1.3.0': + '@img/sharp-libvips-linux-arm64@1.3.1': optional: true '@img/sharp-libvips-linux-arm@1.2.4': optional: true - '@img/sharp-libvips-linux-arm@1.3.0': + '@img/sharp-libvips-linux-arm@1.3.1': optional: true '@img/sharp-libvips-linux-ppc64@1.2.4': optional: true - '@img/sharp-libvips-linux-ppc64@1.3.0': + '@img/sharp-libvips-linux-ppc64@1.3.1': optional: true '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true - '@img/sharp-libvips-linux-riscv64@1.3.0': + '@img/sharp-libvips-linux-riscv64@1.3.1': optional: true '@img/sharp-libvips-linux-s390x@1.2.4': optional: true - '@img/sharp-libvips-linux-s390x@1.3.0': + '@img/sharp-libvips-linux-s390x@1.3.1': optional: true '@img/sharp-libvips-linux-x64@1.2.4': optional: true - '@img/sharp-libvips-linux-x64@1.3.0': + '@img/sharp-libvips-linux-x64@1.3.1': optional: true '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.3.0': + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': optional: true '@img/sharp-libvips-linuxmusl-x64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.3.0': + '@img/sharp-libvips-linuxmusl-x64@1.3.1': optional: true '@img/sharp-linux-arm64@0.34.5': @@ -5002,9 +5242,9 @@ snapshots: '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true - '@img/sharp-linux-arm64@0.35.1': + '@img/sharp-linux-arm64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.3.0 + '@img/sharp-libvips-linux-arm64': 1.3.1 optional: true '@img/sharp-linux-arm@0.34.5': @@ -5012,9 +5252,9 @@ snapshots: '@img/sharp-libvips-linux-arm': 1.2.4 optional: true - '@img/sharp-linux-arm@0.35.1': + '@img/sharp-linux-arm@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.3.0 + '@img/sharp-libvips-linux-arm': 1.3.1 optional: true '@img/sharp-linux-ppc64@0.34.5': @@ -5022,9 +5262,9 @@ snapshots: '@img/sharp-libvips-linux-ppc64': 1.2.4 optional: true - '@img/sharp-linux-ppc64@0.35.1': + '@img/sharp-linux-ppc64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.3.0 + '@img/sharp-libvips-linux-ppc64': 1.3.1 optional: true '@img/sharp-linux-riscv64@0.34.5': @@ -5032,9 +5272,9 @@ snapshots: '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true - '@img/sharp-linux-riscv64@0.35.1': + '@img/sharp-linux-riscv64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.3.0 + '@img/sharp-libvips-linux-riscv64': 1.3.1 optional: true '@img/sharp-linux-s390x@0.34.5': @@ -5042,9 +5282,9 @@ snapshots: '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true - '@img/sharp-linux-s390x@0.35.1': + '@img/sharp-linux-s390x@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.3.0 + '@img/sharp-libvips-linux-s390x': 1.3.1 optional: true '@img/sharp-linux-x64@0.34.5': @@ -5052,9 +5292,9 @@ snapshots: '@img/sharp-libvips-linux-x64': 1.2.4 optional: true - '@img/sharp-linux-x64@0.35.1': + '@img/sharp-linux-x64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.3.0 + '@img/sharp-libvips-linux-x64': 1.3.1 optional: true '@img/sharp-linuxmusl-arm64@0.34.5': @@ -5062,9 +5302,9 @@ snapshots: '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true - '@img/sharp-linuxmusl-arm64@0.35.1': + '@img/sharp-linuxmusl-arm64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.3.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 optional: true '@img/sharp-linuxmusl-x64@0.34.5': @@ -5072,9 +5312,9 @@ snapshots: '@img/sharp-libvips-linuxmusl-x64': 1.2.4 optional: true - '@img/sharp-linuxmusl-x64@0.35.1': + '@img/sharp-linuxmusl-x64@0.35.2': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.3.0 + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 optional: true '@img/sharp-wasm32@0.34.5': @@ -5082,32 +5322,32 @@ snapshots: '@emnapi/runtime': 1.11.1 optional: true - '@img/sharp-wasm32@0.35.1': + '@img/sharp-wasm32@0.35.2': dependencies: '@emnapi/runtime': 1.11.1 optional: true - '@img/sharp-webcontainers-wasm32@0.35.1': + '@img/sharp-webcontainers-wasm32@0.35.2': dependencies: - '@img/sharp-wasm32': 0.35.1 + '@img/sharp-wasm32': 0.35.2 optional: true '@img/sharp-win32-arm64@0.34.5': optional: true - '@img/sharp-win32-arm64@0.35.1': + '@img/sharp-win32-arm64@0.35.2': optional: true '@img/sharp-win32-ia32@0.34.5': optional: true - '@img/sharp-win32-ia32@0.35.1': + '@img/sharp-win32-ia32@0.35.2': optional: true '@img/sharp-win32-x64@0.34.5': optional: true - '@img/sharp-win32-x64@0.35.1': + '@img/sharp-win32-x64@0.35.2': optional: true '@jridgewell/gen-mapping@0.3.13': @@ -5166,8 +5406,31 @@ snapshots: transitivePeerDependencies: - supports-color + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + dependencies: + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 + '@tybys/wasm-util': 0.10.3 + optional: true + '@oslojs/encoding@1.1.0': {} + '@oxc-project/types@0.133.0': {} + '@pagefind/darwin-arm64@1.5.2': optional: true @@ -5257,8 +5520,59 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) + '@rolldown/binding-android-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-x64@1.0.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.3': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.3': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.3': + optional: true + '@rolldown/pluginutils@1.0.0-rc.3': {} + '@rolldown/pluginutils@1.0.1': {} + '@rollup/plugin-babel@5.3.1(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@2.80.0)': dependencies: '@babel/core': 7.29.7 @@ -5309,81 +5623,6 @@ snapshots: optionalDependencies: rollup: 2.80.0 - '@rollup/rollup-android-arm-eabi@4.61.1': - optional: true - - '@rollup/rollup-android-arm64@4.61.1': - optional: true - - '@rollup/rollup-darwin-arm64@4.61.1': - optional: true - - '@rollup/rollup-darwin-x64@4.61.1': - optional: true - - '@rollup/rollup-freebsd-arm64@4.61.1': - optional: true - - '@rollup/rollup-freebsd-x64@4.61.1': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.61.1': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.61.1': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.61.1': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.61.1': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.61.1': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.61.1': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.61.1': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.61.1': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.61.1': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.61.1': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.61.1': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.61.1': - optional: true - - '@rollup/rollup-linux-x64-musl@4.61.1': - optional: true - - '@rollup/rollup-openbsd-x64@4.61.1': - optional: true - - '@rollup/rollup-openharmony-arm64@4.61.1': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.61.1': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.61.1': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.61.1': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.61.1': - optional: true - '@shikijs/core@4.2.0': dependencies: '@shikijs/primitive': 4.2.0 @@ -5431,6 +5670,11 @@ snapshots: magic-string: 0.25.9 string.prototype.matchall: 4.0.12 + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.7 @@ -5470,8 +5714,6 @@ snapshots: '@types/js-yaml@4.0.9': {} - '@types/katex@0.16.8': {} - '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -5508,14 +5750,14 @@ snapshots: '@types/unist@3.0.3': {} - '@ungap/structured-clone@1.3.1': {} + '@ungap/structured-clone@1.3.2': {} - '@vite-pwa/astro@1.2.0(astro@6.4.6(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@7.3.5(@types/node@24.13.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1))': + '@vite-pwa/astro@1.2.0(astro@7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1))': dependencies: - astro: 6.4.6(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - vite-plugin-pwa: 1.3.0(vite@7.3.5(@types/node@24.13.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1) + astro: 7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite-plugin-pwa: 1.3.0(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1) - '@vitejs/plugin-react@5.2.0(vite@7.3.5(@types/node@24.13.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': + '@vitejs/plugin-react@5.2.0(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) @@ -5523,7 +5765,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.5(@types/node@24.13.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -5548,14 +5790,14 @@ snapshots: path-browserify: 1.0.1 request-light: 0.7.0 vscode-languageserver: 9.0.1 - vscode-languageserver-protocol: 3.18.0 + vscode-languageserver-protocol: 3.18.1 vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.1.0 '@volar/language-service@2.4.28': dependencies: '@volar/language-core': 2.4.28 - vscode-languageserver-protocol: 3.18.0 + vscode-languageserver-protocol: 3.18.1 vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.1.0 @@ -5594,6 +5836,10 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + am-i-vibing@0.4.0: + dependencies: + process-ancestry: 0.1.0 + ansi-regex@5.0.1: {} ansi-styles@4.3.0: @@ -5630,21 +5876,22 @@ snapshots: astring@1.9.0: {} - astro-expressive-code@0.43.1(astro@6.4.6(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): + astro-expressive-code@0.43.1(astro@7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): dependencies: - astro: 6.4.6(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + astro: 7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) rehype-expressive-code: 0.43.1 - astro@6.4.6(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): + astro@7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): dependencies: - '@astrojs/compiler': 4.0.0 + '@astrojs/compiler-rs': 0.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1) '@astrojs/internal-helpers': 0.10.0 - '@astrojs/markdown-remark': 7.2.0 + '@astrojs/markdown-satteri': 0.3.2 '@astrojs/telemetry': 3.3.2 '@capsizecss/unpack': 4.0.1 - '@clack/prompts': 1.5.1 + '@clack/prompts': 1.6.0 '@oslojs/encoding': 1.1.0 '@rollup/pluginutils': 5.4.0(rollup@2.80.0) + am-i-vibing: 0.4.0 aria-query: 5.3.2 axobject-query: 4.1.0 ci-info: 4.4.0 @@ -5655,7 +5902,7 @@ snapshots: diff: 8.0.4 dset: 3.1.4 es-module-lexer: 2.1.0 - esbuild: 0.27.7 + esbuild: 0.28.1 flattie: 1.1.1 fontace: 0.4.1 get-tsconfig: 5.0.0-beta.4 @@ -5675,11 +5922,11 @@ snapshots: piccolore: 0.1.3 picomatch: 4.0.4 rehype: 13.0.2 - semver: 7.8.4 + semver: 7.8.5 shiki: 4.2.0 - smol-toml: 1.6.1 + smol-toml: 1.7.0 svgo: 4.0.1 - tinyclip: 0.1.14 + tinyclip: 0.1.15 tinyexec: 1.2.4 tinyglobby: 0.2.17 ultrahtml: 1.6.0 @@ -5687,12 +5934,13 @@ snapshots: unist-util-visit: 5.1.0 unstorage: 1.17.5 vfile: 6.0.3 - vite: 7.3.5(@types/node@24.13.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - vitefu: 1.1.3(vite@7.3.5(@types/node@24.13.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vitefu: 1.1.3(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) xxhash-wasm: 1.1.0 yargs-parser: 22.0.0 zod: 4.4.3 optionalDependencies: + '@astrojs/markdown-remark': 7.2.0 sharp: 0.34.5 transitivePeerDependencies: - '@azure/app-configuration' @@ -5703,6 +5951,8 @@ snapshots: - '@azure/storage-blob' - '@capacitor/preferences' - '@deno/kv' + - '@emnapi/core' + - '@emnapi/runtime' - '@netlify/blobs' - '@planetscale/database' - '@types/node' @@ -5710,19 +5960,18 @@ snapshots: - '@vercel/blob' - '@vercel/functions' - '@vercel/kv' + - '@vitejs/devtools' - aws4fetch - db0 - idb-keyval - ioredis - jiti - less - - lightningcss - rollup - sass - sass-embedded - stylus - sugarss - - supports-color - terser - tsx - uploadthing @@ -5768,7 +6017,7 @@ snapshots: balanced-match@1.0.2: {} - baseline-browser-mapping@2.10.37: {} + baseline-browser-mapping@2.10.38: {} bcp-47-match@2.0.3: {} @@ -5789,13 +6038,13 @@ snapshots: dependencies: balanced-match: 1.0.2 - browserslist@4.28.2: + browserslist@4.28.4: dependencies: - baseline-browser-mapping: 2.10.37 + baseline-browser-mapping: 2.10.38 caniuse-lite: 1.0.30001799 - electron-to-chromium: 1.5.372 - node-releases: 2.0.47 - update-browserslist-db: 1.2.3(browserslist@4.28.2) + electron-to-chromium: 1.5.377 + node-releases: 2.0.48 + update-browserslist-db: 1.2.3(browserslist@4.28.4) buffer-from@1.1.2: {} @@ -5876,7 +6125,7 @@ snapshots: core-js-compat@3.49.0: dependencies: - browserslist: 4.28.2 + browserslist: 4.28.4 crossws@0.3.5: dependencies: @@ -6002,7 +6251,7 @@ snapshots: dependencies: jake: 10.9.4 - electron-to-chromium@1.5.372: {} + electron-to-chromium@1.5.377: {} emmet@2.4.11: dependencies: @@ -6015,6 +6264,13 @@ snapshots: entities@6.0.1: {} + es-abstract-get@1.0.0: + dependencies: + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 + es-abstract@1.24.2: dependencies: array-buffer-byte-length: 1.0.2 @@ -6029,7 +6285,7 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.2 es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 + es-to-primitive: 1.3.1 function.prototype.name: 1.2.0 get-intrinsic: 1.3.0 get-proto: 1.0.1 @@ -6089,8 +6345,10 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 - es-to-primitive@1.3.0: + es-to-primitive@1.3.1: dependencies: + es-abstract-get: 1.0.0 + es-errors: 1.3.0 is-callable: 1.2.7 is-date-object: 1.1.0 is-symbol: 1.1.1 @@ -6109,34 +6367,34 @@ snapshots: esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 - esbuild@0.27.7: + esbuild@0.28.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.7 - '@esbuild/android-arm': 0.27.7 - '@esbuild/android-arm64': 0.27.7 - '@esbuild/android-x64': 0.27.7 - '@esbuild/darwin-arm64': 0.27.7 - '@esbuild/darwin-x64': 0.27.7 - '@esbuild/freebsd-arm64': 0.27.7 - '@esbuild/freebsd-x64': 0.27.7 - '@esbuild/linux-arm': 0.27.7 - '@esbuild/linux-arm64': 0.27.7 - '@esbuild/linux-ia32': 0.27.7 - '@esbuild/linux-loong64': 0.27.7 - '@esbuild/linux-mips64el': 0.27.7 - '@esbuild/linux-ppc64': 0.27.7 - '@esbuild/linux-riscv64': 0.27.7 - '@esbuild/linux-s390x': 0.27.7 - '@esbuild/linux-x64': 0.27.7 - '@esbuild/netbsd-arm64': 0.27.7 - '@esbuild/netbsd-x64': 0.27.7 - '@esbuild/openbsd-arm64': 0.27.7 - '@esbuild/openbsd-x64': 0.27.7 - '@esbuild/openharmony-arm64': 0.27.7 - '@esbuild/sunos-x64': 0.27.7 - '@esbuild/win32-arm64': 0.27.7 - '@esbuild/win32-ia32': 0.27.7 - '@esbuild/win32-x64': 0.27.7 + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 escalade@3.2.0: {} @@ -6258,8 +6516,6 @@ snapshots: functions-have-names@1.2.3: {} - gemoji@8.1.0: {} - generator-function@2.0.1: {} gensync@1.0.0-beta.2: {} @@ -6363,19 +6619,6 @@ snapshots: html-whitespace-sensitive-tag-names: 3.0.1 unist-util-visit-parents: 6.0.2 - hast-util-from-dom@5.0.1: - dependencies: - '@types/hast': 3.0.4 - hastscript: 9.0.1 - web-namespaces: 2.0.1 - - hast-util-from-html-isomorphic@2.0.0: - dependencies: - '@types/hast': 3.0.4 - hast-util-from-dom: 5.0.1 - hast-util-from-html: 2.0.3 - unist-util-remove-position: 5.0.0 - hast-util-from-html@2.0.3: dependencies: '@types/hast': 3.0.4 @@ -6432,7 +6675,7 @@ snapshots: dependencies: '@types/hast': 3.0.4 '@types/unist': 3.0.3 - '@ungap/structured-clone': 1.3.1 + '@ungap/structured-clone': 1.3.2 hast-util-from-parse5: 8.0.3 hast-util-to-parse5: 8.0.1 html-void-elements: 3.0.0 @@ -6564,7 +6807,7 @@ snapshots: idb@7.1.1: {} - immutable@5.1.6: {} + immutable@5.1.7: {} inflight@1.0.6: dependencies: @@ -6762,10 +7005,6 @@ snapshots: jsonpointer@5.0.1: {} - katex@0.16.47: - dependencies: - commander: 8.3.0 - katex@0.17.0: dependencies: commander: 8.3.0 @@ -6776,6 +7015,55 @@ snapshots: leven@3.1.0: {} + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lodash.debounce@4.0.8: {} lodash.sortby@4.7.0: {} @@ -6911,18 +7199,6 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-math@3.0.0: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - devlop: 1.1.0 - longest-streak: 3.1.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - unist-util-remove-position: 5.0.0 - transitivePeerDependencies: - - supports-color - mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 @@ -6981,7 +7257,7 @@ snapshots: dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@ungap/structured-clone': 1.3.1 + '@ungap/structured-clone': 1.3.2 devlop: 1.1.0 micromark-util-sanitize-uri: 2.0.1 trim-lines: 3.0.1 @@ -7096,16 +7372,6 @@ snapshots: micromark-util-combine-extensions: 2.0.1 micromark-util-types: 2.0.2 - micromark-extension-math@3.1.0: - dependencies: - '@types/katex': 0.16.8 - devlop: 1.1.0 - katex: 0.16.47 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - micromark-extension-mdx-expression@3.0.1: dependencies: '@types/estree': 1.0.9 @@ -7307,7 +7573,7 @@ snapshots: muggle-string@0.4.1: {} - nanoid@3.3.12: {} + nanoid@3.3.15: {} neotraverse@0.6.18: {} @@ -7322,7 +7588,7 @@ snapshots: node-mock-http@1.0.4: {} - node-releases@2.0.47: {} + node-releases@2.0.48: {} normalize-path@3.0.0: {} @@ -7445,7 +7711,7 @@ snapshots: postcss@8.5.15: dependencies: - nanoid: 3.3.12 + nanoid: 3.3.15 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -7457,6 +7723,8 @@ snapshots: prismjs@1.30.0: {} + process-ancestry@0.1.0: {} + property-information@7.2.0: {} punycode@2.3.1: {} @@ -7550,13 +7818,13 @@ snapshots: regenerate: 1.4.2 regenerate-unicode-properties: 10.2.2 regjsgen: 0.8.0 - regjsparser: 0.13.1 + regjsparser: 0.13.2 unicode-match-property-ecmascript: 2.0.0 unicode-match-property-value-ecmascript: 2.2.1 regjsgen@0.8.0: {} - regjsparser@0.13.1: + regjsparser@0.13.2: dependencies: jsesc: 3.1.0 @@ -7569,16 +7837,6 @@ snapshots: '@types/hast': 3.0.4 hast-util-format: 1.1.0 - rehype-katex@7.0.1: - dependencies: - '@types/hast': 3.0.4 - '@types/katex': 0.16.8 - hast-util-from-html-isomorphic: 2.0.0 - hast-util-to-text: 4.0.2 - katex: 0.16.47 - unist-util-visit-parents: 6.0.2 - vfile: 6.0.3 - rehype-parse@9.0.1: dependencies: '@types/hast': 3.0.4 @@ -7621,12 +7879,6 @@ snapshots: transitivePeerDependencies: - supports-color - remark-gemoji@8.0.0: - dependencies: - '@types/mdast': 4.0.4 - gemoji: 8.1.0 - mdast-util-find-and-replace: 3.0.2 - remark-gfm@4.0.1: dependencies: '@types/mdast': 4.0.4 @@ -7638,20 +7890,6 @@ snapshots: transitivePeerDependencies: - supports-color - remark-heading-id@1.0.1: - dependencies: - lodash: 4.18.1 - unist-util-visit: 1.4.1 - - remark-math@6.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-math: 3.0.0 - micromark-extension-math: 3.1.0 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - remark-mdx@3.1.1: dependencies: mdast-util-mdx: 3.0.0 @@ -7731,39 +7969,29 @@ snapshots: retext-stringify: 4.0.0 unified: 11.0.5 - rollup@2.80.0: + rolldown@1.0.3: + dependencies: + '@oxc-project/types': 0.133.0 + '@rolldown/pluginutils': 1.0.1 optionalDependencies: - fsevents: 2.3.3 + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 - rollup@4.61.1: - dependencies: - '@types/estree': 1.0.9 + rollup@2.80.0: optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.61.1 - '@rollup/rollup-android-arm64': 4.61.1 - '@rollup/rollup-darwin-arm64': 4.61.1 - '@rollup/rollup-darwin-x64': 4.61.1 - '@rollup/rollup-freebsd-arm64': 4.61.1 - '@rollup/rollup-freebsd-x64': 4.61.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.61.1 - '@rollup/rollup-linux-arm-musleabihf': 4.61.1 - '@rollup/rollup-linux-arm64-gnu': 4.61.1 - '@rollup/rollup-linux-arm64-musl': 4.61.1 - '@rollup/rollup-linux-loong64-gnu': 4.61.1 - '@rollup/rollup-linux-loong64-musl': 4.61.1 - '@rollup/rollup-linux-ppc64-gnu': 4.61.1 - '@rollup/rollup-linux-ppc64-musl': 4.61.1 - '@rollup/rollup-linux-riscv64-gnu': 4.61.1 - '@rollup/rollup-linux-riscv64-musl': 4.61.1 - '@rollup/rollup-linux-s390x-gnu': 4.61.1 - '@rollup/rollup-linux-x64-gnu': 4.61.1 - '@rollup/rollup-linux-x64-musl': 4.61.1 - '@rollup/rollup-openbsd-x64': 4.61.1 - '@rollup/rollup-openharmony-arm64': 4.61.1 - '@rollup/rollup-win32-arm64-msvc': 4.61.1 - '@rollup/rollup-win32-ia32-msvc': 4.61.1 - '@rollup/rollup-win32-x64-gnu': 4.61.1 - '@rollup/rollup-win32-x64-msvc': 4.61.1 fsevents: 2.3.3 safe-array-concat@1.1.4: @@ -7790,18 +8018,31 @@ snapshots: sass@1.101.0: dependencies: chokidar: 5.0.0 - immutable: 5.1.6 + immutable: 5.1.7 source-map-js: 1.2.1 optionalDependencies: '@parcel/watcher': 2.5.6 + satteri@0.9.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + optionalDependencies: + '@bruits/satteri-darwin-arm64': 0.9.1 + '@bruits/satteri-darwin-x64': 0.9.1 + '@bruits/satteri-linux-x64-gnu': 0.9.1 + '@bruits/satteri-wasm32-wasi': 0.9.1 + '@bruits/satteri-win32-x64-msvc': 0.9.1 + sax@1.6.0: {} scheduler@0.27.0: {} semver@6.3.1: {} - semver@7.8.4: {} + semver@7.8.5: {} serialize-javascript@6.0.2: dependencies: @@ -7833,7 +8074,7 @@ snapshots: dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 - semver: 7.8.4 + semver: 7.8.5 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-x64': 0.34.5 @@ -7861,37 +8102,37 @@ snapshots: '@img/sharp-win32-x64': 0.34.5 optional: true - sharp@0.35.1: + sharp@0.35.2: dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 - semver: 7.8.4 + semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.35.1 - '@img/sharp-darwin-x64': 0.35.1 - '@img/sharp-freebsd-wasm32': 0.35.1 - '@img/sharp-libvips-darwin-arm64': 1.3.0 - '@img/sharp-libvips-darwin-x64': 1.3.0 - '@img/sharp-libvips-linux-arm': 1.3.0 - '@img/sharp-libvips-linux-arm64': 1.3.0 - '@img/sharp-libvips-linux-ppc64': 1.3.0 - '@img/sharp-libvips-linux-riscv64': 1.3.0 - '@img/sharp-libvips-linux-s390x': 1.3.0 - '@img/sharp-libvips-linux-x64': 1.3.0 - '@img/sharp-libvips-linuxmusl-arm64': 1.3.0 - '@img/sharp-libvips-linuxmusl-x64': 1.3.0 - '@img/sharp-linux-arm': 0.35.1 - '@img/sharp-linux-arm64': 0.35.1 - '@img/sharp-linux-ppc64': 0.35.1 - '@img/sharp-linux-riscv64': 0.35.1 - '@img/sharp-linux-s390x': 0.35.1 - '@img/sharp-linux-x64': 0.35.1 - '@img/sharp-linuxmusl-arm64': 0.35.1 - '@img/sharp-linuxmusl-x64': 0.35.1 - '@img/sharp-webcontainers-wasm32': 0.35.1 - '@img/sharp-win32-arm64': 0.35.1 - '@img/sharp-win32-ia32': 0.35.1 - '@img/sharp-win32-x64': 0.35.1 + '@img/sharp-darwin-arm64': 0.35.2 + '@img/sharp-darwin-x64': 0.35.2 + '@img/sharp-freebsd-wasm32': 0.35.2 + '@img/sharp-libvips-darwin-arm64': 1.3.1 + '@img/sharp-libvips-darwin-x64': 1.3.1 + '@img/sharp-libvips-linux-arm': 1.3.1 + '@img/sharp-libvips-linux-arm64': 1.3.1 + '@img/sharp-libvips-linux-ppc64': 1.3.1 + '@img/sharp-libvips-linux-riscv64': 1.3.1 + '@img/sharp-libvips-linux-s390x': 1.3.1 + '@img/sharp-libvips-linux-x64': 1.3.1 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 + '@img/sharp-linux-arm': 0.35.2 + '@img/sharp-linux-arm64': 0.35.2 + '@img/sharp-linux-ppc64': 0.35.2 + '@img/sharp-linux-riscv64': 0.35.2 + '@img/sharp-linux-s390x': 0.35.2 + '@img/sharp-linux-x64': 0.35.2 + '@img/sharp-linuxmusl-arm64': 0.35.2 + '@img/sharp-linuxmusl-x64': 0.35.2 + '@img/sharp-webcontainers-wasm32': 0.35.2 + '@img/sharp-win32-arm64': 0.35.2 + '@img/sharp-win32-ia32': 0.35.2 + '@img/sharp-win32-x64': 0.35.2 shiki@4.2.0: dependencies: @@ -7943,7 +8184,7 @@ snapshots: smob@1.6.2: {} - smol-toml@1.6.1: {} + smol-toml@1.7.0: {} source-map-js@1.2.1: {} @@ -8072,7 +8313,7 @@ snapshots: tiny-inflate@1.0.3: {} - tinyclip@0.1.14: {} + tinyclip@0.1.15: {} tinyexec@1.2.4: {} @@ -8131,7 +8372,7 @@ snapshots: typescript-auto-import-cache@0.3.6: dependencies: - semver: 7.8.4 + semver: 7.8.5 typescript@6.0.3: {} @@ -8186,8 +8427,6 @@ snapshots: '@types/unist': 3.0.3 unist-util-is: 6.0.1 - unist-util-is@3.0.0: {} - unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -8218,19 +8457,11 @@ snapshots: dependencies: '@types/unist': 3.0.3 - unist-util-visit-parents@2.1.2: - dependencies: - unist-util-is: 3.0.0 - unist-util-visit-parents@6.0.2: dependencies: '@types/unist': 3.0.3 unist-util-is: 6.0.1 - unist-util-visit@1.4.1: - dependencies: - unist-util-visit-parents: 2.1.2 - unist-util-visit@5.1.0: dependencies: '@types/unist': 3.0.3 @@ -8252,9 +8483,9 @@ snapshots: upath@1.2.0: {} - update-browserslist-db@1.2.3(browserslist@4.28.2): + update-browserslist-db@1.2.3(browserslist@4.28.4): dependencies: - browserslist: 4.28.2 + browserslist: 4.28.4 escalade: 3.2.0 picocolors: 1.1.1 @@ -8275,35 +8506,35 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-pwa@1.3.0(vite@7.3.5(@types/node@24.13.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1): + vite-plugin-pwa@1.3.0(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1): dependencies: debug: 4.4.3 pretty-bytes: 6.1.1 tinyglobby: 0.2.17 - vite: 7.3.5(@types/node@24.13.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) workbox-build: 7.3.0(@types/babel__core@7.20.5) workbox-window: 7.4.1 transitivePeerDependencies: - supports-color - vite@7.3.5(@types/node@24.13.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): + vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): dependencies: - esbuild: 0.27.7 - fdir: 6.5.0(picomatch@4.0.4) + lightningcss: 1.32.0 picomatch: 4.0.4 postcss: 8.5.15 - rollup: 4.61.1 + rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.13.2 + esbuild: 0.28.1 fsevents: 2.3.3 sass: 1.101.0 terser: 5.48.0 yaml: 2.9.0 - vitefu@1.1.3(vite@7.3.5(@types/node@24.13.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): + vitefu@1.1.3(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): optionalDependencies: - vite: 7.3.5(@types/node@24.13.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) volar-service-css@0.0.70(@volar/language-service@2.4.28): dependencies: @@ -8346,7 +8577,7 @@ snapshots: volar-service-typescript@0.0.70(@volar/language-service@2.4.28): dependencies: path-browserify: 1.0.1 - semver: 7.8.4 + semver: 7.8.5 typescript-auto-import-cache: 0.3.6 vscode-languageserver-textdocument: 1.0.12 vscode-nls: 5.2.0 @@ -8392,7 +8623,7 @@ snapshots: vscode-jsonrpc: 8.2.0 vscode-languageserver-types: 3.17.5 - vscode-languageserver-protocol@3.18.0: + vscode-languageserver-protocol@3.18.1: dependencies: vscode-jsonrpc: 9.0.0 vscode-languageserver-types: 3.18.0 @@ -8620,7 +8851,7 @@ snapshots: yargs-parser@22.0.0: {} - yargs@17.7.2: + yargs@17.7.3: dependencies: cliui: 8.0.1 escalade: 3.2.0 diff --git a/docs/pnpm-workspace.yaml b/docs/pnpm-workspace.yaml index f0ce1ab865f3..40576d87373a 100644 --- a/docs/pnpm-workspace.yaml +++ b/docs/pnpm-workspace.yaml @@ -2,3 +2,7 @@ allowBuilds: '@parcel/watcher': true esbuild: true sharp: true +minimumReleaseAgeExclude: + - '@astrojs/markdown-satteri@0.3.2' + - '@astrojs/starlight@0.41.0' + - astro@7.0.2 diff --git a/docs/src/content/docs/af/community/faq.md b/docs/src/content/docs/af/community/faq.md index a9fd0c3cb4ce..cb37981d4617 100644 --- a/docs/src/content/docs/af/community/faq.md +++ b/docs/src/content/docs/af/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/af/community/stylus-support.md b/docs/src/content/docs/af/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/af/community/stylus-support.md +++ b/docs/src/content/docs/af/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/af/docs/v2/tools/label.md b/docs/src/content/docs/af/docs/v2/tools/label.md index 90b646225eb3..92cd16632f19 100644 --- a/docs/src/content/docs/af/docs/v2/tools/label.md +++ b/docs/src/content/docs/af/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/ar/community/faq.md b/docs/src/content/docs/ar/community/faq.md index 15b872913ce4..53f923c3418b 100644 --- a/docs/src/content/docs/ar/community/faq.md +++ b/docs/src/content/docs/ar/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, أنا أستخدم المدخل الذي يوفره إطار التردد. هناك بالفعل مشاكل لتعقبها: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) و [flutter#102836](https://github.com/flutter/flutter/issues/102836). لينوكس: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) وهذه المسائل قديمة إلى حد ما. يجب أن تعمل نسخة الويب بشكل جيد في الوقت الحالي. diff --git a/docs/src/content/docs/ar/community/stylus-support.md b/docs/src/content/docs/ar/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/ar/community/stylus-support.md +++ b/docs/src/content/docs/ar/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/ar/docs/v2/tools/label.md b/docs/src/content/docs/ar/docs/v2/tools/label.md index 1feb7629aaf5..c09444ac65bb 100644 --- a/docs/src/content/docs/ar/docs/v2/tools/label.md +++ b/docs/src/content/docs/ar/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/ca/community/faq.md b/docs/src/content/docs/ca/community/faq.md index 5d00eabe33af..9f13698324a6 100644 --- a/docs/src/content/docs/ca/community/faq.md +++ b/docs/src/content/docs/ca/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/ca/community/stylus-support.md b/docs/src/content/docs/ca/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/ca/community/stylus-support.md +++ b/docs/src/content/docs/ca/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/ca/docs/v2/tools/label.md b/docs/src/content/docs/ca/docs/v2/tools/label.md index 93214a290c4a..2be71f8899a4 100644 --- a/docs/src/content/docs/ca/docs/v2/tools/label.md +++ b/docs/src/content/docs/ca/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/community/faq.md b/docs/src/content/docs/community/faq.md index 908b7392590e..5429de7c7725 100644 --- a/docs/src/content/docs/community/faq.md +++ b/docs/src/content/docs/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/community/stylus-support.md b/docs/src/content/docs/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/community/stylus-support.md +++ b/docs/src/content/docs/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/cs/community/faq.md b/docs/src/content/docs/cs/community/faq.md index 29a2e61f32e9..ed99c3546c12 100644 --- a/docs/src/content/docs/cs/community/faq.md +++ b/docs/src/content/docs/cs/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/cs/community/stylus-support.md b/docs/src/content/docs/cs/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/cs/community/stylus-support.md +++ b/docs/src/content/docs/cs/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/cs/docs/v2/tools/label.md b/docs/src/content/docs/cs/docs/v2/tools/label.md index 6f2b906e9c6f..46456c31cc0f 100644 --- a/docs/src/content/docs/cs/docs/v2/tools/label.md +++ b/docs/src/content/docs/cs/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/da/community/faq.md b/docs/src/content/docs/da/community/faq.md index f75df9dab195..13adfe28b68c 100644 --- a/docs/src/content/docs/da/community/faq.md +++ b/docs/src/content/docs/da/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/da/community/stylus-support.md b/docs/src/content/docs/da/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/da/community/stylus-support.md +++ b/docs/src/content/docs/da/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/da/docs/v2/tools/label.md b/docs/src/content/docs/da/docs/v2/tools/label.md index 0382d98ca178..bb0f8d61fe33 100644 --- a/docs/src/content/docs/da/docs/v2/tools/label.md +++ b/docs/src/content/docs/da/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/de/community/faq.md b/docs/src/content/docs/de/community/faq.md index 692f7cb4a3aa..3262f2cfc2f3 100644 --- a/docs/src/content/docs/de/community/faq.md +++ b/docs/src/content/docs/de/community/faq.md @@ -64,7 +64,6 @@ Siehe [Stiftunterstützung](/community/stylus-support) für aktuelles Verhalten, Ich benutze die Eingabe des Flutter-Frameworks. Es gibt bereits Issues zum Nachverfolgen: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) und [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) Diese Probleme sind etwas älter. Die Web-Version sollte im Moment gut funktionieren. diff --git a/docs/src/content/docs/de/community/stylus-support.md b/docs/src/content/docs/de/community/stylus-support.md index a19a917740e6..5d36c7485abe 100644 --- a/docs/src/content/docs/de/community/stylus-support.md +++ b/docs/src/content/docs/de/community/stylus-support.md @@ -7,7 +7,7 @@ Diese Seite sammelt das aktuelle Verhalten von Stift- und Stylus-Eingaben in But ## Unterstützt platforms - **Android / mobil:** Stylus-Eingabe funktioniert derzeit am besten und ist der wichtigste unterstützte Weg. -- **Desktop (Windows/Linux):** Stylus-Eingabe ist aktuell durch die Eingabeunterstützung von Flutter eingeschränkt. +- **Desktop (Linux):** Stylus-Eingabe ist aktuell durch die Eingabeunterstützung von Flutter eingeschränkt. - **Web:** Oft eine gute Ausweichmöglichkeit, wenn sich ein Stift auf dem Desktop uneinheitlich verhält. ## Stylus-related Einstellungen @@ -33,7 +33,6 @@ Diese Werte sind Bit-Zuordnungen aus Pointer-Button-Flags. Einige Stylus-Probleme entstehen durch die Eingabeverarbeitung von Flutter und nicht direkt durch Butterfly. - Android S-Stift tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows-Stiftprobleme: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux-Stiftproblem: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) Wenn Ihr Setup betroffen ist, melden Sie es bitte trotzdem in den Butterfly-Issues mit Plattform, Gerätemodell und App-Version. diff --git a/docs/src/content/docs/de/docs/v2/tools/label.md b/docs/src/content/docs/de/docs/v2/tools/label.md index e8c940667751..27fda82a3115 100644 --- a/docs/src/content/docs/de/docs/v2/tools/label.md +++ b/docs/src/content/docs/de/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. Ein Beispielbefehl ist `\int_{7}^{6}`, mit dem das Integralsymbol angezeigt wird: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/docs/v2/tools/label.md b/docs/src/content/docs/docs/v2/tools/label.md index 9d3ab7e07a80..4ef9565b623a 100644 --- a/docs/src/content/docs/docs/v2/tools/label.md +++ b/docs/src/content/docs/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/el/community/faq.md b/docs/src/content/docs/el/community/faq.md index c2fb97686af2..bb532a981b00 100644 --- a/docs/src/content/docs/el/community/faq.md +++ b/docs/src/content/docs/el/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/el/community/stylus-support.md b/docs/src/content/docs/el/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/el/community/stylus-support.md +++ b/docs/src/content/docs/el/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/el/docs/v2/tools/label.md b/docs/src/content/docs/el/docs/v2/tools/label.md index 65db702a0a4f..56fd35b0a953 100644 --- a/docs/src/content/docs/el/docs/v2/tools/label.md +++ b/docs/src/content/docs/el/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/es/community/faq.md b/docs/src/content/docs/es/community/faq.md index fd7e657fc928..8bb6eefbeeee 100644 --- a/docs/src/content/docs/es/community/faq.md +++ b/docs/src/content/docs/es/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, Estoy usando la entrada proporcionada por el framework de flujos. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) Estas cuestiones son un poco más antiguas. La versión web debería funcionar bien por ahora. diff --git a/docs/src/content/docs/es/community/stylus-support.md b/docs/src/content/docs/es/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/es/community/stylus-support.md +++ b/docs/src/content/docs/es/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/es/docs/v2/tools/label.md b/docs/src/content/docs/es/docs/v2/tools/label.md index 94061ccd9866..d537bf78fbfb 100644 --- a/docs/src/content/docs/es/docs/v2/tools/label.md +++ b/docs/src/content/docs/es/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/fi/community/faq.md b/docs/src/content/docs/fi/community/faq.md index dc46491cc3cb..0957d1b8d56a 100644 --- a/docs/src/content/docs/fi/community/faq.md +++ b/docs/src/content/docs/fi/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/fi/community/stylus-support.md b/docs/src/content/docs/fi/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/fi/community/stylus-support.md +++ b/docs/src/content/docs/fi/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/fi/docs/v2/tools/label.md b/docs/src/content/docs/fi/docs/v2/tools/label.md index 380d90aaef4f..dab3fc390097 100644 --- a/docs/src/content/docs/fi/docs/v2/tools/label.md +++ b/docs/src/content/docs/fi/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/fr/community/faq.md b/docs/src/content/docs/fr/community/faq.md index 9443ae3ffd27..1d7bf8b1cb0c 100644 --- a/docs/src/content/docs/fr/community/faq.md +++ b/docs/src/content/docs/fr/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, J'utilise l'entrée fournie par le cadre flutter. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) Ces questions sont un peu plus anciennes. La version web devrait fonctionner correctement pour le moment. diff --git a/docs/src/content/docs/fr/community/stylus-support.md b/docs/src/content/docs/fr/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/fr/community/stylus-support.md +++ b/docs/src/content/docs/fr/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/fr/docs/v2/tools/label.md b/docs/src/content/docs/fr/docs/v2/tools/label.md index 2c56372740bd..9f8e509b0c19 100644 --- a/docs/src/content/docs/fr/docs/v2/tools/label.md +++ b/docs/src/content/docs/fr/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/he/community/faq.md b/docs/src/content/docs/he/community/faq.md index 163e0401ae0c..09e89362ad29 100644 --- a/docs/src/content/docs/he/community/faq.md +++ b/docs/src/content/docs/he/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/he/community/stylus-support.md b/docs/src/content/docs/he/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/he/community/stylus-support.md +++ b/docs/src/content/docs/he/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/he/docs/v2/tools/label.md b/docs/src/content/docs/he/docs/v2/tools/label.md index 4a75aaa7be41..52869a3d7d92 100644 --- a/docs/src/content/docs/he/docs/v2/tools/label.md +++ b/docs/src/content/docs/he/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/hi/community/faq.md b/docs/src/content/docs/hi/community/faq.md index 7a7c085b6d47..bc4ae2370c6d 100644 --- a/docs/src/content/docs/hi/community/faq.md +++ b/docs/src/content/docs/hi/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/hi/community/stylus-support.md b/docs/src/content/docs/hi/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/hi/community/stylus-support.md +++ b/docs/src/content/docs/hi/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/hi/docs/v2/tools/label.md b/docs/src/content/docs/hi/docs/v2/tools/label.md index d22327ace45f..4828d0e94681 100644 --- a/docs/src/content/docs/hi/docs/v2/tools/label.md +++ b/docs/src/content/docs/hi/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/hu/community/faq.md b/docs/src/content/docs/hu/community/faq.md index 25a2a7f97148..b50f4a048cb2 100644 --- a/docs/src/content/docs/hu/community/faq.md +++ b/docs/src/content/docs/hu/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/hu/community/stylus-support.md b/docs/src/content/docs/hu/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/hu/community/stylus-support.md +++ b/docs/src/content/docs/hu/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/hu/docs/v2/tools/label.md b/docs/src/content/docs/hu/docs/v2/tools/label.md index 560aa2a430a1..54ca21872c4f 100644 --- a/docs/src/content/docs/hu/docs/v2/tools/label.md +++ b/docs/src/content/docs/hu/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/id/community/faq.md b/docs/src/content/docs/id/community/faq.md index c8b795221276..bb05deb33117 100644 --- a/docs/src/content/docs/id/community/faq.md +++ b/docs/src/content/docs/id/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/id/community/stylus-support.md b/docs/src/content/docs/id/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/id/community/stylus-support.md +++ b/docs/src/content/docs/id/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/id/docs/v2/tools/label.md b/docs/src/content/docs/id/docs/v2/tools/label.md index ef4870032dae..5d99e94cea47 100644 --- a/docs/src/content/docs/id/docs/v2/tools/label.md +++ b/docs/src/content/docs/id/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/it/community/faq.md b/docs/src/content/docs/it/community/faq.md index 41764abbedaa..3dac298a3b82 100644 --- a/docs/src/content/docs/it/community/faq.md +++ b/docs/src/content/docs/it/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, Sto usando l'input fornito dal framework flutter. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) Questi problemi sono un po' più vecchi. La versione web dovrebbe funzionare bene per ora. diff --git a/docs/src/content/docs/it/community/stylus-support.md b/docs/src/content/docs/it/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/it/community/stylus-support.md +++ b/docs/src/content/docs/it/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/it/docs/v2/tools/label.md b/docs/src/content/docs/it/docs/v2/tools/label.md index 1a3512c8d486..976a4f8a1a1c 100644 --- a/docs/src/content/docs/it/docs/v2/tools/label.md +++ b/docs/src/content/docs/it/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/ja/community/faq.md b/docs/src/content/docs/ja/community/faq.md index 25c426aa0cc9..a9a1e78c650f 100644 --- a/docs/src/content/docs/ja/community/faq.md +++ b/docs/src/content/docs/ja/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, フラッターフレームワークで提供される入力を使っています。 There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) これらの問題は少し古いです。 ウェブバージョンは今のところ正常に動作するはずです。 diff --git a/docs/src/content/docs/ja/community/stylus-support.md b/docs/src/content/docs/ja/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/ja/community/stylus-support.md +++ b/docs/src/content/docs/ja/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/ja/docs/v2/tools/label.md b/docs/src/content/docs/ja/docs/v2/tools/label.md index 2ef55c61699e..cc0c4b2d1e37 100644 --- a/docs/src/content/docs/ja/docs/v2/tools/label.md +++ b/docs/src/content/docs/ja/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/ko/community/faq.md b/docs/src/content/docs/ko/community/faq.md index f7b2fcdafa28..53bfb6d0680b 100644 --- a/docs/src/content/docs/ko/community/faq.md +++ b/docs/src/content/docs/ko/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/ko/community/stylus-support.md b/docs/src/content/docs/ko/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/ko/community/stylus-support.md +++ b/docs/src/content/docs/ko/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/ko/docs/v2/tools/label.md b/docs/src/content/docs/ko/docs/v2/tools/label.md index 1f8fc14d26b1..b2f422d48dbe 100644 --- a/docs/src/content/docs/ko/docs/v2/tools/label.md +++ b/docs/src/content/docs/ko/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/nl/community/faq.md b/docs/src/content/docs/nl/community/faq.md index 234dd2f9142e..50ee2eec1ab8 100644 --- a/docs/src/content/docs/nl/community/faq.md +++ b/docs/src/content/docs/nl/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, Ik gebruik de input van het flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) Deze kwesties zijn een beetje ouder. De webversie zou voorlopig prima moeten werken. diff --git a/docs/src/content/docs/nl/community/stylus-support.md b/docs/src/content/docs/nl/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/nl/community/stylus-support.md +++ b/docs/src/content/docs/nl/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/nl/docs/v2/tools/label.md b/docs/src/content/docs/nl/docs/v2/tools/label.md index 72c1bdc7becf..e2f7464ace99 100644 --- a/docs/src/content/docs/nl/docs/v2/tools/label.md +++ b/docs/src/content/docs/nl/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/no/community/faq.md b/docs/src/content/docs/no/community/faq.md index fb7a6127f347..4e0a0538facf 100644 --- a/docs/src/content/docs/no/community/faq.md +++ b/docs/src/content/docs/no/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/no/community/stylus-support.md b/docs/src/content/docs/no/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/no/community/stylus-support.md +++ b/docs/src/content/docs/no/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/no/docs/v2/tools/label.md b/docs/src/content/docs/no/docs/v2/tools/label.md index a7ba615ce510..4abcb92332a1 100644 --- a/docs/src/content/docs/no/docs/v2/tools/label.md +++ b/docs/src/content/docs/no/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/or/community/faq.md b/docs/src/content/docs/or/community/faq.md index aa2afd91f763..20e6c77f8be9 100644 --- a/docs/src/content/docs/or/community/faq.md +++ b/docs/src/content/docs/or/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/or/community/stylus-support.md b/docs/src/content/docs/or/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/or/community/stylus-support.md +++ b/docs/src/content/docs/or/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/or/docs/v2/tools/label.md b/docs/src/content/docs/or/docs/v2/tools/label.md index 083c9dfc6e97..0a306bb10ad6 100644 --- a/docs/src/content/docs/or/docs/v2/tools/label.md +++ b/docs/src/content/docs/or/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/pl/community/faq.md b/docs/src/content/docs/pl/community/faq.md index ff6377b7b335..7b476fda9306 100644 --- a/docs/src/content/docs/pl/community/faq.md +++ b/docs/src/content/docs/pl/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, Używam danych wejściowych dostarczanych przez ramę wytrząsania. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) Te kwestie są nieco starsze. Na razie wersja internetowa powinna działać dobrze. diff --git a/docs/src/content/docs/pl/community/stylus-support.md b/docs/src/content/docs/pl/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/pl/community/stylus-support.md +++ b/docs/src/content/docs/pl/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/pl/docs/v2/tools/label.md b/docs/src/content/docs/pl/docs/v2/tools/label.md index c129b4a84c22..3a7023ed1d84 100644 --- a/docs/src/content/docs/pl/docs/v2/tools/label.md +++ b/docs/src/content/docs/pl/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/pt-br/community/faq.md b/docs/src/content/docs/pt-br/community/faq.md index 8e227039e4e5..48a3ee84d267 100644 --- a/docs/src/content/docs/pt-br/community/faq.md +++ b/docs/src/content/docs/pt-br/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, Eu estou usando os dados fornecidos pelo framework de agitação. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) Estas questões são um pouco mais antigas. A versão web deve funcionar bem por enquanto. diff --git a/docs/src/content/docs/pt-br/community/stylus-support.md b/docs/src/content/docs/pt-br/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/pt-br/community/stylus-support.md +++ b/docs/src/content/docs/pt-br/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/pt-br/docs/v2/tools/label.md b/docs/src/content/docs/pt-br/docs/v2/tools/label.md index e43663c9d7d6..2589f2af491a 100644 --- a/docs/src/content/docs/pt-br/docs/v2/tools/label.md +++ b/docs/src/content/docs/pt-br/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/pt/community/faq.md b/docs/src/content/docs/pt/community/faq.md index 3a8a6e3ad9fd..cecc49215142 100644 --- a/docs/src/content/docs/pt/community/faq.md +++ b/docs/src/content/docs/pt/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, Eu estou usando os dados fornecidos pelo framework de agitação. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) Estas questões são um pouco mais antigas. A versão web deve funcionar bem por enquanto. diff --git a/docs/src/content/docs/pt/community/stylus-support.md b/docs/src/content/docs/pt/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/pt/community/stylus-support.md +++ b/docs/src/content/docs/pt/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/pt/docs/v2/tools/label.md b/docs/src/content/docs/pt/docs/v2/tools/label.md index c41e5d98a369..1eb3aab561da 100644 --- a/docs/src/content/docs/pt/docs/v2/tools/label.md +++ b/docs/src/content/docs/pt/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/ro/community/faq.md b/docs/src/content/docs/ro/community/faq.md index 2d48d8503de5..b25a275112ec 100644 --- a/docs/src/content/docs/ro/community/faq.md +++ b/docs/src/content/docs/ro/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/ro/community/stylus-support.md b/docs/src/content/docs/ro/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/ro/community/stylus-support.md +++ b/docs/src/content/docs/ro/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/ro/docs/v2/tools/label.md b/docs/src/content/docs/ro/docs/v2/tools/label.md index eb8d5021b853..7bf860559536 100644 --- a/docs/src/content/docs/ro/docs/v2/tools/label.md +++ b/docs/src/content/docs/ro/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/ru/community/faq.md b/docs/src/content/docs/ru/community/faq.md index 74010a56563c..02f1e3ed2693 100644 --- a/docs/src/content/docs/ru/community/faq.md +++ b/docs/src/content/docs/ru/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, Я использую вход, предоставленный флэттер. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) Эти проблемы несколько выше. Теперь веб-версия должна работать нормально. diff --git a/docs/src/content/docs/ru/community/stylus-support.md b/docs/src/content/docs/ru/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/ru/community/stylus-support.md +++ b/docs/src/content/docs/ru/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/ru/docs/v2/tools/label.md b/docs/src/content/docs/ru/docs/v2/tools/label.md index 8a5c0f810778..549961f81299 100644 --- a/docs/src/content/docs/ru/docs/v2/tools/label.md +++ b/docs/src/content/docs/ru/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/sr/community/faq.md b/docs/src/content/docs/sr/community/faq.md index c6aa895ccc44..586f4a3879d8 100644 --- a/docs/src/content/docs/sr/community/faq.md +++ b/docs/src/content/docs/sr/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/sr/community/stylus-support.md b/docs/src/content/docs/sr/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/sr/community/stylus-support.md +++ b/docs/src/content/docs/sr/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/sr/docs/v2/tools/label.md b/docs/src/content/docs/sr/docs/v2/tools/label.md index 7388a2c20d4e..af0c8ba20dd3 100644 --- a/docs/src/content/docs/sr/docs/v2/tools/label.md +++ b/docs/src/content/docs/sr/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/sv/community/faq.md b/docs/src/content/docs/sv/community/faq.md index f26a3488858f..67c5db1b4298 100644 --- a/docs/src/content/docs/sv/community/faq.md +++ b/docs/src/content/docs/sv/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/sv/community/stylus-support.md b/docs/src/content/docs/sv/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/sv/community/stylus-support.md +++ b/docs/src/content/docs/sv/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/sv/docs/v2/tools/label.md b/docs/src/content/docs/sv/docs/v2/tools/label.md index e52ec1a7a667..6ba05dfa8e12 100644 --- a/docs/src/content/docs/sv/docs/v2/tools/label.md +++ b/docs/src/content/docs/sv/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/th/community/faq.md b/docs/src/content/docs/th/community/faq.md index 0eb39fed3511..15e684a65d9f 100644 --- a/docs/src/content/docs/th/community/faq.md +++ b/docs/src/content/docs/th/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/th/community/stylus-support.md b/docs/src/content/docs/th/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/th/community/stylus-support.md +++ b/docs/src/content/docs/th/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/th/docs/v2/tools/label.md b/docs/src/content/docs/th/docs/v2/tools/label.md index bfb7221b0616..777bf7690b9c 100644 --- a/docs/src/content/docs/th/docs/v2/tools/label.md +++ b/docs/src/content/docs/th/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/tr/community/faq.md b/docs/src/content/docs/tr/community/faq.md index a49d86ab68cb..b1e661bde136 100644 --- a/docs/src/content/docs/tr/community/faq.md +++ b/docs/src/content/docs/tr/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, Flutter çerçevesi tarafından sağlanan girdiyi kullanıyorum. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) Bu konular biraz eski. Web sürümü şuan iyi çalışmalıdır. diff --git a/docs/src/content/docs/tr/community/stylus-support.md b/docs/src/content/docs/tr/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/tr/community/stylus-support.md +++ b/docs/src/content/docs/tr/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/tr/docs/v2/tools/label.md b/docs/src/content/docs/tr/docs/v2/tools/label.md index a897b3d6d17d..7abb37c91ddd 100644 --- a/docs/src/content/docs/tr/docs/v2/tools/label.md +++ b/docs/src/content/docs/tr/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/uk/community/faq.md b/docs/src/content/docs/uk/community/faq.md index 46c05767ee8a..c747257ffc6a 100644 --- a/docs/src/content/docs/uk/community/faq.md +++ b/docs/src/content/docs/uk/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/uk/community/stylus-support.md b/docs/src/content/docs/uk/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/uk/community/stylus-support.md +++ b/docs/src/content/docs/uk/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/uk/docs/v2/tools/label.md b/docs/src/content/docs/uk/docs/v2/tools/label.md index 54dbcdcd24cd..ffc7fd640651 100644 --- a/docs/src/content/docs/uk/docs/v2/tools/label.md +++ b/docs/src/content/docs/uk/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/vi/community/faq.md b/docs/src/content/docs/vi/community/faq.md index 142d8ab29df7..f9e04ec847e4 100644 --- a/docs/src/content/docs/vi/community/faq.md +++ b/docs/src/content/docs/vi/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/vi/community/stylus-support.md b/docs/src/content/docs/vi/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/vi/community/stylus-support.md +++ b/docs/src/content/docs/vi/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/vi/docs/v2/tools/label.md b/docs/src/content/docs/vi/docs/v2/tools/label.md index 5eaffed2d82b..b36d471b9df4 100644 --- a/docs/src/content/docs/vi/docs/v2/tools/label.md +++ b/docs/src/content/docs/vi/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/zh-hant/community/faq.md b/docs/src/content/docs/zh-hant/community/faq.md index 627a6e4531d5..150867c9193c 100644 --- a/docs/src/content/docs/zh-hant/community/faq.md +++ b/docs/src/content/docs/zh-hant/community/faq.md @@ -64,7 +64,6 @@ See [Stylus support](/community/stylus-support) for current behavior, settings, I'm using the input provided by the flutter framework. There are already issues to track it: -Windows: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) and [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) These issues are a bit older. The web version should work fine for now. diff --git a/docs/src/content/docs/zh-hant/community/stylus-support.md b/docs/src/content/docs/zh-hant/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/zh-hant/community/stylus-support.md +++ b/docs/src/content/docs/zh-hant/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/zh-hant/docs/v2/tools/label.md b/docs/src/content/docs/zh-hant/docs/v2/tools/label.md index b2f8fab2e900..1779aa28fd9f 100644 --- a/docs/src/content/docs/zh-hant/docs/v2/tools/label.md +++ b/docs/src/content/docs/zh-hant/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands diff --git a/docs/src/content/docs/zh/community/faq.md b/docs/src/content/docs/zh/community/faq.md index 7cb95698c7f6..f49cb9597158 100644 --- a/docs/src/content/docs/zh/community/faq.md +++ b/docs/src/content/docs/zh/community/faq.md @@ -63,7 +63,6 @@ Butterfly在iOS下有预览版。 点击此[链接](https://butterfly.linwood.de See [Stylus support](/community/stylus-support) for current behavior, settings, and issue links. 我使用流体框架提供的输入。 -查看相关issue: Windows[flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248) 以及 [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836). Linux: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) 这些问题比以前更长。 网络版本现在应该很好。 diff --git a/docs/src/content/docs/zh/community/stylus-support.md b/docs/src/content/docs/zh/community/stylus-support.md index 351bb8e5db0c..397acb36feb9 100644 --- a/docs/src/content/docs/zh/community/stylus-support.md +++ b/docs/src/content/docs/zh/community/stylus-support.md @@ -7,7 +7,7 @@ This page collects the current stylus and pen-input behavior in Butterfly. ## Supported platforms - **Android / mobile:** Stylus input works best and is the main supported path today. -- **Desktop (Windows/Linux):** Stylus input is currently limited by Flutter input support. +- **Desktop (Linux):** Stylus input is currently limited by Flutter input support. - **Web:** Often a good fallback when desktop stylus behavior is inconsistent. ## Stylus-related settings @@ -33,7 +33,6 @@ These values are bit mappings from pointer button flags. Some stylus issues come from Flutter input handling rather than Butterfly directly. - Android S-Pen tracking issue: [flutter/flutter#42846](https://github.com/flutter/flutter/issues/42846) -- Windows stylus issues: [flutter/flutter#65248](https://github.com/flutter/flutter/issues/65248), [flutter/flutter#102836](https://github.com/flutter/flutter/issues/102836) - Linux stylus issue: [flutter/flutter#63209](https://github.com/flutter/flutter/issues/63209) If your setup is affected, please still report it in Butterfly issues with your platform, device model, and app version. diff --git a/docs/src/content/docs/zh/docs/v2/tools/label.md b/docs/src/content/docs/zh/docs/v2/tools/label.md index 8e5727a505c6..5d941afab5b5 100644 --- a/docs/src/content/docs/zh/docs/v2/tools/label.md +++ b/docs/src/content/docs/zh/docs/v2/tools/label.md @@ -41,9 +41,9 @@ A backslash (`\`) must precede any LaTeX command to indicate it is a command. An example command is `\int_{7}^{6}`, which is used to show the integral symbol: -```math +$$ \int_{7}^{6} -``` +$$ ### Useful commands From 73c2287f5a552980e605fc5c6d50013f54ccc28f Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Fri, 26 Jun 2026 17:25:22 +0200 Subject: [PATCH 012/117] Improve state management for better linking different systems together --- app/lib/actions/change_path.dart | 12 +- app/lib/actions/zoom.dart | 2 +- app/lib/bloc/document_bloc.dart | 16 +- app/lib/bloc/document_state.dart | 23 +- app/lib/cubits/current_index.dart | 194 +++++------ app/lib/cubits/current_index.freezed.dart | 33 +- app/lib/dialogs/collaboration/dialog.dart | 2 +- app/lib/dialogs/export/general.dart | 1 - app/lib/handlers/eraser.dart | 6 +- app/lib/handlers/laser.dart | 6 +- app/lib/handlers/pen.dart | 4 +- app/lib/handlers/stamp.dart | 2 +- app/lib/services/import.dart | 14 +- app/lib/views/app_bar.dart | 402 +++++++++++----------- app/lib/views/navigator/files.dart | 188 +++++----- app/lib/views/pen_only_toggle.dart | 4 +- app/lib/views/view.dart | 20 +- app/pubspec.lock | 18 +- app/pubspec.yaml | 2 +- app/test/bloc/document_bloc_test.dart | 3 +- metadata/en-US/changelogs/187.txt | 2 + 21 files changed, 460 insertions(+), 494 deletions(-) diff --git a/app/lib/actions/change_path.dart b/app/lib/actions/change_path.dart index 09f37893f94d..d4a9bbd3e27e 100644 --- a/app/lib/actions/change_path.dart +++ b/app/lib/actions/change_path.dart @@ -27,9 +27,10 @@ class ChangePathAction extends Action { @override Future invoke(ChangePathIntent intent) async { final bloc = context.read(); - final state = bloc.state; - if (state is! DocumentLoadSuccess || state.location.path == '') return; - final location = state.location; + final cubit = bloc.currentIndexCubit; + final cubitState = cubit.state; + if (cubitState.location.path == '') return; + final location = cubitState.location; final settings = context.read().state; final fileSystem = context.read().buildDocumentSystem( settings.getRemote(location.remote), @@ -45,10 +46,7 @@ class ChangePathAction extends Action { ), ); if (newLocations == null) return; - bloc.currentIndexCubit.setSaveState( - location: newLocations.first, - isCreating: false, - ); + cubit.setSaveState(location: newLocations.first, isCreating: false); bloc.save(); } } diff --git a/app/lib/actions/zoom.dart b/app/lib/actions/zoom.dart index 2cfba51dff5a..b2151e5e177e 100644 --- a/app/lib/actions/zoom.dart +++ b/app/lib/actions/zoom.dart @@ -36,7 +36,7 @@ class ZoomAction extends Action { (viewport.width ?? 0) / 2, (viewport.height ?? 0) / 2, ); - final transformCubit = currentIndex.transformCubit; + final transformCubit = cubit.transformCubit; cubit.size( transformCubit.state.size + (intent.reverse ? -0.1 : 0.1), center, diff --git a/app/lib/bloc/document_bloc.dart b/app/lib/bloc/document_bloc.dart index edd09adf2542..ea664e1bc776 100644 --- a/app/lib/bloc/document_bloc.dart +++ b/app/lib/bloc/document_bloc.dart @@ -149,9 +149,9 @@ class DocumentBloc extends ReplayBloc { AssetService? _assetService; CurrentIndexCubit get currentIndexCubit => _currentIndexCubit!; - TransformCubit get transformCubit => currentIndexCubit.state.transformCubit; + TransformCubit get transformCubit => currentIndexCubit.transformCubit; NetworkingService? get networkingService => - _currentIndexCubit?.state.networkingService; + _currentIndexCubit?.networkingService; Embedding? get embedding => _currentIndexCubit?.state.embedding; factory DocumentBloc( @@ -172,7 +172,6 @@ class DocumentBloc extends ReplayBloc { currentIndexCubit, windowCubit, initial, - location, resolvedAssetService, page, pageName, @@ -185,7 +184,6 @@ class DocumentBloc extends ReplayBloc { CurrentIndexCubit currentIndexCubit, WindowCubit windowCubit, NoteData initial, - AssetLocation location, AssetService assetService, [ DocumentPage? page, String? pageName, @@ -198,7 +196,6 @@ class DocumentBloc extends ReplayBloc { page: page, assetService: assetService, windowCubit: windowCubit, - location: location, absolute: absolute, fileSystem: fileSystem, pageName: pageName ?? initial.getPages(true).firstOrNull ?? '', @@ -1480,7 +1477,6 @@ class DocumentBloc extends ReplayBloc { metadata: current.metadata, fileSystem: current.fileSystem, windowCubit: current.windowCubit, - location: current.location, absolute: current.absolute, ); currentIndexCubit.updateHandler(this, newState.handler); @@ -1709,7 +1705,7 @@ class DocumentBloc extends ReplayBloc { final current = state; final cubit = _currentIndexCubit; if (current is! DocumentLoaded || cubit == null) return; - if (!current.location.isEmpty) { + if (!cubit.state.location.isEmpty) { cubit.setSaveState( saved: SaveState.saved, isCreating: false, @@ -1788,7 +1784,7 @@ class DocumentBloc extends ReplayBloc { clearHistory(); final currentState = state; final currentIndexCubit = _currentIndexCubit; - final transformCubit = currentIndexCubit?.state.transformCubit; + final transformCubit = currentIndexCubit?.transformCubit; final assetService = _assetService; if (currentIndexCubit != null && !currentIndexCubit.isClosed) { await currentIndexCubit.close(); @@ -1856,7 +1852,7 @@ class DocumentBloc extends ReplayBloc { final state = this.state; final cubit = _currentIndexCubit; if (state is! DocumentLoadSuccess || cubit == null) return {}; - transform ??= cubit.state.transformCubit.state; + transform ??= cubit.transformCubit.state; final renderers = cubit.state.cameraViewport.visibleElements; if (renderers.isEmpty) return {}; hitElementMode ??= HitElementMode.touchAnywhere; @@ -1893,7 +1889,7 @@ class DocumentBloc extends ReplayBloc { if (state is! DocumentLoadSuccess || cubit == null) return {}; final renderers = cubit.state.cameraViewport.visibleElements; if (renderers.isEmpty) return {}; - transform ??= cubit.state.transformCubit.state; + transform ??= cubit.transformCubit.state; hitElementMode ??= HitElementMode.touchAnywhere; final params = _RayCastPolygonParams( diff --git a/app/lib/bloc/document_state.dart b/app/lib/bloc/document_state.dart index fc189fc9d3a0..110a5eb3fae6 100644 --- a/app/lib/bloc/document_state.dart +++ b/app/lib/bloc/document_state.dart @@ -61,7 +61,6 @@ abstract class DocumentLoaded extends DocumentState { final FileMetadata metadata; @override final AssetService assetService; - final AssetLocation location; final bool absolute; Future _updatePage(NoteData current) async => @@ -80,7 +79,6 @@ abstract class DocumentLoaded extends DocumentState { AssetService? assetService, FileMetadata? metadata, DocumentInfo? info, - required this.location, required this.absolute, }) : page = page ?? data.getPage(pageName) ?? DocumentDefaults.createPage(), assetService = assetService ?? AssetService(), @@ -126,7 +124,6 @@ class DocumentLoadSuccess extends DocumentLoaded { required super.pageName, super.metadata, super.info, - AssetLocation? location, super.absolute = false, this.storageType = StorageType.local, String? currentAreaName, @@ -142,8 +139,7 @@ class DocumentLoadSuccess extends DocumentLoaded { currentLayer = currentLayer ?? (page ?? data.getPage(pageName))?.layers.lastOrNull?.id ?? - createUniqueId(), - super(location: location ?? const AssetLocation(path: '')); + createUniqueId(); @override Area? get currentArea { @@ -174,25 +170,11 @@ class DocumentLoadSuccess extends DocumentLoaded { currentAreaName: currentAreaName ?? this.currentAreaName, fileSystem: fileSystem, windowCubit: windowCubit, - location: location, absolute: absolute, ); bool isLayerVisible(String? layer) => !invisibleLayers.contains(layer); - bool hasAutosave(NetworkingService networkingService, Embedding? embedding) => - settingsCubit.state.autosave && - (networkingService.isActive || - !(embedding?.save ?? true) || - (!kIsWeb && - !absolute && - (location.isEmpty || (location.fileType?.isNote() ?? false)) && - (location.remote.isEmpty || - (settingsCubit - .getRemote(location.remote) - ?.hasDocumentCached(location.path) ?? - false)))); - AreaPreset areaPreset(CameraViewport cameraViewport) => AreaPreset(name: pageName, area: cameraViewport.toArea(), page: pageName); @@ -221,7 +203,6 @@ class DocumentPresentationState extends DocumentLoaded { required super.windowCubit, required super.pageName, required super.assetService, - required super.location, required super.absolute, }) : handler = PresentationStateHandler(track, bloc), super(oldState.data); @@ -238,7 +219,6 @@ class DocumentPresentationState extends DocumentLoaded { required super.windowCubit, required super.pageName, required super.assetService, - required super.location, required super.absolute, }) : super(oldState.data); @@ -258,7 +238,6 @@ class DocumentPresentationState extends DocumentLoaded { pageName: pageName, fileSystem: fileSystem, windowCubit: windowCubit, - location: location, absolute: absolute, ); } diff --git a/app/lib/cubits/current_index.dart b/app/lib/cubits/current_index.dart index a366f099b906..f6ffcdd9d42a 100644 --- a/app/lib/cubits/current_index.dart +++ b/app/lib/cubits/current_index.dart @@ -54,10 +54,7 @@ sealed class CurrentIndex with _$CurrentIndex { const factory CurrentIndex( int? index, Handler handler, - CameraViewport cameraViewport, - SettingsCubit settingsCubit, - TransformCubit transformCubit, - NetworkingService networkingService, { + CameraViewport cameraViewport, { @Default(false) bool isSaveDelayed, @Default(UtilitiesState()) UtilitiesState utilities, Handler? temporaryHandler, @@ -95,19 +92,6 @@ sealed class CurrentIndex with _$CurrentIndex { @Default(false) bool sessionPenOnlyInput, }) = _CurrentIndex; - /// Returns the effective pen-only input state. - /// If the setting is null (auto), uses the session-based state. - /// Otherwise uses the persisted setting. - bool get effectivePenOnlyInput { - final setting = settingsCubit.state.penOnlyInput; - if (setting != null) return setting; - return sessionPenOnlyInput; - } - - bool get moveEnabled => - (settingsCubit.state.inputGestures && pointers.length > 1) && - settingsCubit.state.moveOnGesture; - bool get absolute => saved == SaveState.absoluteRead; MouseCursor get currentCursor => temporaryCursor ?? cursor; @@ -126,21 +110,23 @@ sealed class CurrentIndex with _$CurrentIndex { } class CurrentIndexCubit extends Cubit { + final SettingsCubit settingsCubit; + final TransformCubit transformCubit; + final NetworkingService networkingService; + CurrentIndexCubit( - SettingsCubit settingsCubit, - TransformCubit transformCubit, + this.settingsCubit, + this.transformCubit, CameraViewport viewport, { Embedding? embedding, NetworkingService? networkingService, bool absolute = false, - }) : super( + }) : networkingService = networkingService ?? NetworkingService(), + super( CurrentIndex( null, HandHandler(), viewport, - settingsCubit, - transformCubit, - networkingService ?? NetworkingService(), embedding: embedding, saved: absolute ? SaveState.absoluteRead : SaveState.saved, ), @@ -164,6 +150,19 @@ class CurrentIndexCubit extends Cubit { return true; } + /// Returns the effective pen-only input state. + /// If the setting is null (auto), uses the session-based state. + /// Otherwise uses the persisted setting. + bool get effectivePenOnlyInput { + final setting = settingsCubit.state.penOnlyInput; + if (setting != null) return setting; + return state.sessionPenOnlyInput; + } + + bool get moveEnabled => + (settingsCubit.state.inputGestures && state.pointers.length > 1) && + settingsCubit.state.moveOnGesture; + void _onTransformChanged(CameraTransform transform) { // Debounce transform changes to avoid excessive updates during pan/zoom _transformDebounceTimer?.cancel(); @@ -229,7 +228,7 @@ class CurrentIndexCubit extends Cubit { void init(DocumentBloc bloc) { _documentBloc = WeakReference(bloc); changeTool(bloc, index: state.index ?? 0); - state.networkingService.setup(bloc); + networkingService.setup(bloc); } void setPenDetected(bool detected) { @@ -237,7 +236,7 @@ class CurrentIndexCubit extends Cubit { // When pen is detected and setting is auto (null), enable session pen-only final shouldEnableSessionPenOnly = detected && - state.settingsCubit.state.penOnlyInput == null && + settingsCubit.state.penOnlyInput == null && !state.sessionPenOnlyInput; emit( state.copyWith( @@ -279,7 +278,7 @@ class CurrentIndexCubit extends Cubit { if (newVisible.isEmpty && newlyHidden.isEmpty) return; - final transform = renderTransform ?? state.transformCubit.state; + final transform = renderTransform ?? transformCubit.state; final size = targetSize ?? newViewport.toSize(); _initializedElements.removeAll(newlyHidden); @@ -320,8 +319,7 @@ class CurrentIndexCubit extends Cubit { bool dark, [ VisualDensity? density, ColorScheme? overridden, - ]) => - getThemeData(state.settingsCubit.state.design, dark, density, overridden); + ]) => getThemeData(settingsCubit.state.design, dark, density, overridden); Handler getHandler({bool disableTemporary = false}) { if (state.embedding?.editable == false) { @@ -375,7 +373,7 @@ class CurrentIndexCubit extends Cubit { await Future.wait( foregrounds.map( (e) async => await e.setup( - state.transformCubit, + transformCubit, document, blocState.assetService, blocState.page, @@ -461,14 +459,14 @@ class CurrentIndexCubit extends Cubit { Offset? cursor, }) { cursor ??= state.lastPosition ?? Offset.zero; - state.networkingService.sendUser( + networkingService.sendUser( NetworkingUser( - cursor: state.transformCubit.state.localToGlobal(cursor).toPoint(), + cursor: transformCubit.state.localToGlobal(cursor).toPoint(), foreground: (foregrounds ?? state.getAllForegrounds(false)) .map((e) => e.element) .whereType() .toList(), - name: state.networkingService.userName, + name: networkingService.userName, ), ); } @@ -480,7 +478,7 @@ class CurrentIndexCubit extends Cubit { talker.verbose('Updating networking state'); final blocState = bloc.state; if (blocState is! DocumentLoadSuccess) return; - final users = (current ?? state.networkingService.users).entries.toList(); + final users = (current ?? networkingService.users).entries.toList(); final usersByChannel = {for (final entry in users) entry.key: entry.value}; final activeForegroundElements = users .expand((entry) => entry.value.foreground ?? const []) @@ -525,7 +523,7 @@ class CurrentIndexCubit extends Cubit { await Future.wait( added.map( (e) async => await e.setup( - state.transformCubit, + transformCubit, blocState.data, blocState.assetService, blocState.page, @@ -574,7 +572,7 @@ class CurrentIndexCubit extends Cubit { await Future.wait( foregrounds.map( (e) async => await e.setup( - state.transformCubit, + transformCubit, docState.data, docState.assetService, docState.page, @@ -612,7 +610,7 @@ class CurrentIndexCubit extends Cubit { await Future.wait( foregrounds.map( (e) async => await e.setup( - state.transformCubit, + transformCubit, docState.data, docState.assetService, docState.page, @@ -723,12 +721,8 @@ class CurrentIndexCubit extends Cubit { state.temporaryHandler?.setupForegrounds == true) { await Future.wait( temporaryForegrounds.map( - (e) async => await e.setup( - state.transformCubit, - document, - assetService, - page, - ), + (e) async => + await e.setup(transformCubit, document, assetService, page), ), ); } @@ -742,12 +736,8 @@ class CurrentIndexCubit extends Cubit { if (state.handler.setupForegrounds) { await Future.wait( foregrounds.map( - (e) async => await e.setup( - state.transformCubit, - document, - assetService, - page, - ), + (e) async => + await e.setup(transformCubit, document, assetService, page), ), ); } @@ -765,12 +755,8 @@ class CurrentIndexCubit extends Cubit { if (handler.setupForegrounds) { await Future.wait( foregrounds.map( - (e) async => await e.setup( - state.transformCubit, - document, - assetService, - page, - ), + (e) async => + await e.setup(transformCubit, document, assetService, page), ), ); } @@ -846,7 +832,7 @@ class CurrentIndexCubit extends Cubit { await Future.wait( temporaryForegrounds.map( (e) async => - await e.setup(state.transformCubit, document, assetService, page), + await e.setup(transformCubit, document, assetService, page), ), ); } @@ -862,7 +848,7 @@ class CurrentIndexCubit extends Cubit { await Future.wait( foregrounds.map( (e) async => - await e.setup(state.transformCubit, document, assetService, page), + await e.setup(transformCubit, document, assetService, page), ), ); } @@ -952,7 +938,7 @@ class CurrentIndexCubit extends Cubit { await Future.wait( foregrounds.map( (e) async => await e.setup( - state.transformCubit, + transformCubit, document, blocState.assetService, page, @@ -1107,7 +1093,7 @@ class CurrentIndexCubit extends Cubit { await Future.wait( temporaryForegrounds.map( (e) async => await e.setup( - state.transformCubit, + transformCubit, document, blocState.assetService, page, @@ -1177,8 +1163,8 @@ class CurrentIndexCubit extends Cubit { Rect getViewportRect({Size? viewportSize}) { var size = viewportSize ?? state.cameraViewport.toSize(); - final transform = state.transformCubit.state; - final resolution = state.settingsCubit.state.renderResolution; + final transform = transformCubit.state; + final resolution = settingsCubit.state.renderResolution; final friction = transform.friction; final realWidth = size.width / transform.size; @@ -1266,9 +1252,9 @@ class CurrentIndexCubit extends Cubit { }) => _bakeLock.synchronized(() async { if (isClosed) return; var cameraViewport = state.cameraViewport; - final startTransform = state.transformCubit.state; + final startTransform = transformCubit.state; final startViewport = cameraViewport; - final resolution = state.settingsCubit.state.renderResolution; + final resolution = settingsCubit.state.renderResolution; var size = viewportSize ?? cameraViewport.toSize(); final ratio = pixelRatio ?? cameraViewport.pixelRatio; if (size.height <= 0 || size.width <= 0) { @@ -1277,7 +1263,7 @@ class CurrentIndexCubit extends Cubit { if (viewportSize == null) { size /= resolution.multiplier; } - var transform = state.transformCubit.state; + var transform = transformCubit.state; var renderers = List>.from(this.renderers); final recorder = ui.PictureRecorder(); final canvas = ui.Canvas(recorder); @@ -1490,7 +1476,7 @@ class CurrentIndexCubit extends Cubit { // If state changed while baking (e.g. fast move submitted a newer viewport), // this bake output is stale and must not overwrite the latest viewport. final currentViewport = state.cameraViewport; - final currentTransform = state.transformCubit.state; + final currentTransform = transformCubit.state; if (!identical(currentViewport, startViewport) || currentTransform != startTransform) { newImage.dispose(); @@ -1764,7 +1750,7 @@ class CurrentIndexCubit extends Cubit { await Future.wait( newRenderers.map( (e) async => - await e.setup(state.transformCubit, document, assetService, page), + await e.setup(transformCubit, document, assetService, page), ), ); // Build layer index map for O(1) lookups instead of O(n) indexOf calls @@ -1793,7 +1779,7 @@ class CurrentIndexCubit extends Cubit { await Future.wait( backgrounds.map( (e) async => - await e.setup(state.transformCubit, document, assetService, page), + await e.setup(transformCubit, document, assetService, page), ), ); final rect = getViewportRect(); @@ -1899,7 +1885,7 @@ class CurrentIndexCubit extends Cubit { renderBackground: renderBackground, ), cameraViewport: await CameraViewport.build( - state.transformCubit, + transformCubit, document, docState.assetService, page, @@ -2075,14 +2061,14 @@ class CurrentIndexCubit extends Cubit { ); emit(state); if (utilities != null) { - return state.settingsCubit.changeUtilities(utilities); + return settingsCubit.changeUtilities(utilities); } } void togglePin() => emit(state.copyWith(pinned: !state.pinned)); bool _isNavigationRailVisible() { - final settings = state.settingsCubit.state; + final settings = settingsCubit.state; final viewport = state.cameraViewport; return settings.navigationRail && settings.navigatorPosition == NavigatorPosition.left && @@ -2095,14 +2081,14 @@ class CurrentIndexCubit extends Cubit { Area? currentArea, CameraTransform? customTransform, ]) { - final settings = state.settingsCubit.state; + final settings = settingsCubit.state; var multiplier = settings.limitViewportMultiplier; final positive = settings.limitViewportPositive; if (multiplier == null && !positive && currentArea == null) return null; final viewport = state.cameraViewport; - final transform = customTransform ?? state.transformCubit.state; + final transform = customTransform ?? transformCubit.state; final navigationRailOffset = _isNavigationRailVisible() ? kNavigationRailWidth / transform.size : 0.0; @@ -2182,7 +2168,7 @@ class CurrentIndexCubit extends Cubit { final newBounds = _calculateViewportBounds(area); if (newBounds == null) return; - final pos = state.transformCubit.state.position; + final pos = transformCubit.state.position; double newX = pos.dx; double newY = pos.dy; if (dx > 0) { @@ -2199,7 +2185,7 @@ class CurrentIndexCubit extends Cubit { } else { newY = newY.clamp(newBounds.top, newBounds.bottom); } - state.transformCubit.teleport(Offset(newX, newY)); + transformCubit.teleport(Offset(newX, newY)); } Future navigateToRelativeArea( @@ -2249,7 +2235,7 @@ class CurrentIndexCubit extends Cubit { final bounds = _calculateViewportBounds(currentArea); if (bounds != null) { - final pos = state.transformCubit.state.position; + final pos = transformCubit.state.position; var newPos = pos + delta; final clampedPos = Offset( newPos.dx.clamp(bounds.left, bounds.right), @@ -2273,7 +2259,7 @@ class CurrentIndexCubit extends Cubit { } if ((dx != 0 || dy != 0) && - state.settingsCubit.state.hasFlag('edgePanAreaSwitching')) { + settingsCubit.state.hasFlag('edgePanAreaSwitching')) { final area = getRelativeArea(currentArea, dx, dy); if (area != null) { _activeDocumentBloc?.add(CurrentAreaChanged(area.name)); @@ -2290,7 +2276,7 @@ class CurrentIndexCubit extends Cubit { if (delta.dx == 0 && delta.dy == 0) { return; } - state.transformCubit.move(delta); + transformCubit.move(delta); } void zoom(double delta, [Offset cursor = Offset.zero, bool force = false]) { @@ -2302,28 +2288,28 @@ class CurrentIndexCubit extends Cubit { return; } if (force) { - state.transformCubit.zoom(delta, cursor); + transformCubit.zoom(delta, cursor); return; } - final transform = state.transformCubit.state.withSize( - state.transformCubit.state.size * delta, + final transform = transformCubit.state.withSize( + transformCubit.state.size * delta, cursor, ); final clamped = _clampTransform(transform); - state.transformCubit.teleport(clamped.position, clamped.size); + transformCubit.teleport(clamped.position, clamped.size); } void size(double size, [Offset cursor = Offset.zero, bool force = false]) { final utilitiesState = state.utilities; if (utilitiesState.lockZoom && !force) return; if (force) { - state.transformCubit.size(size, cursor); + transformCubit.size(size, cursor); return; } final transform = _clampTransform( - state.transformCubit.state.withSize(size, cursor), + transformCubit.state.withSize(size, cursor), ); - state.transformCubit.teleport(transform.position, transform.size); + transformCubit.teleport(transform.position, transform.size); } void slide( @@ -2332,7 +2318,7 @@ class CurrentIndexCubit extends Cubit { bool force = false, Area? currentArea, }) { - final settings = state.settingsCubit.state; + final settings = settingsCubit.state; if (!settings.hasFlag('smoothNavigation')) return; final utilitiesState = state.utilities; Rect? bounds; @@ -2348,7 +2334,7 @@ class CurrentIndexCubit extends Cubit { bounds = _calculateViewportBounds(currentArea); if (bounds != null) { - final pos = state.transformCubit.state.position; + final pos = transformCubit.state.position; final clampedPos = Offset( pos.dx.clamp(bounds.left, bounds.right), pos.dy.clamp(bounds.top, bounds.bottom), @@ -2383,7 +2369,7 @@ class CurrentIndexCubit extends Cubit { return; } cancelDelayedBake(); - state.transformCubit.slide( + transformCubit.slide( positionVelocity, sizeVelocity, positionBounds: bounds, @@ -2403,10 +2389,24 @@ class CurrentIndexCubit extends Cubit { void exitHideUI() => emit(state.copyWith(hideUi: HideState.visible)); ExternalStorage? getRemoteStorage() => - state.settingsCubit.getRemote(state.location.remote); + settingsCubit.getRemote(state.location.remote); final _savingLock = Lock(); + bool hasAutosave() => + settingsCubit.state.autosave && + (networkingService.isActive || + !(state.embedding?.save ?? true) || + (!kIsWeb && + !state.absolute && + (state.location.isEmpty || + (state.location.fileType?.isNote() ?? false)) && + (state.location.remote.isEmpty || + (settingsCubit + .getRemote(state.location.remote) + ?.hasDocumentCached(state.location.path) ?? + false)))); + Future save( DocumentBloc bloc, { AssetLocation? location, @@ -2419,7 +2419,7 @@ class CurrentIndexCubit extends Cubit { state.saved == SaveState.absoluteRead)) { return state.location; } - if (state.networkingService.isClient) { + if (networkingService.isClient) { return AssetLocation.empty; } if (state.isSaveDelayed && isAutosave) { @@ -2427,9 +2427,9 @@ class CurrentIndexCubit extends Cubit { } final storage = getRemoteStorage(); final fileSystem = bloc.state.fileSystem.buildDocumentSystem(storage); - final isDelayed = state.settingsCubit.state.delayedAutosave; + final isDelayed = settingsCubit.state.delayedAutosave; if (isDelayed && isAutosave) { - final seconds = max(0, state.settingsCubit.state.autosaveDelaySeconds); + final seconds = max(0, settingsCubit.state.autosaveDelaySeconds); emit(state.copyWith(isSaveDelayed: true)); await Future.delayed(Duration(seconds: seconds)); if (!state.isSaveDelayed) { @@ -2477,7 +2477,7 @@ class CurrentIndexCubit extends Cubit { )); await fileSystem.updateFile(current.path, file); } - state.settingsCubit.addRecentHistory(current); + settingsCubit.addRecentHistory(current); if (isClosed) { return current; } @@ -2519,8 +2519,8 @@ class CurrentIndexCubit extends Cubit { _networkingDebounceTimer = null; await _delayedBakeRunner.disposeAndWait(); await _foregroundRefreshRunner.disposeAndWait(); - if (!currentState.networkingService.isClosed) { - await currentState.networkingService.close(); + if (!networkingService.isClosed) { + await networkingService.close(); } return super.close(); } @@ -2558,7 +2558,7 @@ class CurrentIndexCubit extends Cubit { ...addedElements, }) { await renderer.setup( - state.transformCubit, + transformCubit, current.data, current.assetService, current.page, @@ -2610,7 +2610,7 @@ class CurrentIndexCubit extends Cubit { if (updateIndex) { this.updateIndex(bloc); } - if (current.hasAutosave(state.networkingService, state.embedding)) { + if (hasAutosave()) { save(bloc, isAutosave: true); } } @@ -2659,7 +2659,7 @@ class CurrentIndexCubit extends Cubit { await Future.wait( foregrounds.map( (e) async => await e.setup( - state.transformCubit, + transformCubit, document, blocState.assetService, page, @@ -2697,7 +2697,7 @@ class CurrentIndexCubit extends Cubit { bool reset = false, bool testTransform = false, }) => _delayedBakeRunner.schedule(() async { - final newTransform = state.transformCubit.state; + final newTransform = transformCubit.state; final viewport = state.cameraViewport; if (testTransform && diff --git a/app/lib/cubits/current_index.freezed.dart b/app/lib/cubits/current_index.freezed.dart index a2c706507f10..9251b318d286 100644 --- a/app/lib/cubits/current_index.freezed.dart +++ b/app/lib/cubits/current_index.freezed.dart @@ -14,7 +14,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$CurrentIndex implements DiagnosticableTreeMixin { - int? get index; Handler get handler; CameraViewport get cameraViewport; SettingsCubit get settingsCubit; TransformCubit get transformCubit; NetworkingService get networkingService; bool get isSaveDelayed; UtilitiesState get utilities; Handler? get temporaryHandler; int? get temporaryIndex; List get foregrounds; Selection? get selection; bool get pinned; List? get temporaryForegrounds; Map> get toggleableHandlers; List get networkingForegrounds; Map> get toggleableForegrounds; MouseCursor get cursor; MouseCursor? get temporaryCursor; TemporaryState get temporaryState; Offset? get lastPosition; List get pointers; int? get buttons; AssetLocation get location; Embedding? get embedding; SaveState get saved; PreferredSizeWidget? get toolbar; PreferredSizeWidget? get temporaryToolbar; Map get rendererStates; Map? get temporaryRendererStates; ViewOption get viewOption; HideState get hideUi; bool get areaNavigatorCreate; bool get areaNavigatorExact; bool get areaNavigatorAsk; bool get navigatorEnabled; NavigatorPage get navigatorPage; bool get isCreating; String get userName; bool get penDetected; bool get sessionPenOnlyInput; + int? get index; Handler get handler; CameraViewport get cameraViewport; bool get isSaveDelayed; UtilitiesState get utilities; Handler? get temporaryHandler; int? get temporaryIndex; List get foregrounds; Selection? get selection; bool get pinned; List? get temporaryForegrounds; Map> get toggleableHandlers; List get networkingForegrounds; Map> get toggleableForegrounds; MouseCursor get cursor; MouseCursor? get temporaryCursor; TemporaryState get temporaryState; Offset? get lastPosition; List get pointers; int? get buttons; AssetLocation get location; Embedding? get embedding; SaveState get saved; PreferredSizeWidget? get toolbar; PreferredSizeWidget? get temporaryToolbar; Map get rendererStates; Map? get temporaryRendererStates; ViewOption get viewOption; HideState get hideUi; bool get areaNavigatorCreate; bool get areaNavigatorExact; bool get areaNavigatorAsk; bool get navigatorEnabled; NavigatorPage get navigatorPage; bool get isCreating; String get userName; bool get penDetected; bool get sessionPenOnlyInput; /// Create a copy of CurrentIndex /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -26,14 +26,14 @@ $CurrentIndexCopyWith get copyWith => _$CurrentIndexCopyWithImpl { factory $CurrentIndexCopyWith(CurrentIndex value, $Res Function(CurrentIndex) _then) = _$CurrentIndexCopyWithImpl; @useResult $Res call({ - int? index, Handler handler, CameraViewport cameraViewport, SettingsCubit settingsCubit, TransformCubit transformCubit, NetworkingService networkingService, bool isSaveDelayed, UtilitiesState utilities, Handler? temporaryHandler, int? temporaryIndex, List foregrounds, Selection? selection, bool pinned, List? temporaryForegrounds, Map> toggleableHandlers, List networkingForegrounds, Map> toggleableForegrounds, MouseCursor cursor, MouseCursor? temporaryCursor, TemporaryState temporaryState, Offset? lastPosition, List pointers, int? buttons, AssetLocation location, Embedding? embedding, SaveState saved, PreferredSizeWidget? toolbar, PreferredSizeWidget? temporaryToolbar, Map rendererStates, Map? temporaryRendererStates, ViewOption viewOption, HideState hideUi, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, bool navigatorEnabled, NavigatorPage navigatorPage, bool isCreating, String userName, bool penDetected, bool sessionPenOnlyInput + int? index, Handler handler, CameraViewport cameraViewport, bool isSaveDelayed, UtilitiesState utilities, Handler? temporaryHandler, int? temporaryIndex, List foregrounds, Selection? selection, bool pinned, List? temporaryForegrounds, Map> toggleableHandlers, List networkingForegrounds, Map> toggleableForegrounds, MouseCursor cursor, MouseCursor? temporaryCursor, TemporaryState temporaryState, Offset? lastPosition, List pointers, int? buttons, AssetLocation location, Embedding? embedding, SaveState saved, PreferredSizeWidget? toolbar, PreferredSizeWidget? temporaryToolbar, Map rendererStates, Map? temporaryRendererStates, ViewOption viewOption, HideState hideUi, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, bool navigatorEnabled, NavigatorPage navigatorPage, bool isCreating, String userName, bool penDetected, bool sessionPenOnlyInput }); @@ -61,15 +61,12 @@ class _$CurrentIndexCopyWithImpl<$Res> /// Create a copy of CurrentIndex /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? index = freezed,Object? handler = null,Object? cameraViewport = null,Object? settingsCubit = null,Object? transformCubit = null,Object? networkingService = null,Object? isSaveDelayed = null,Object? utilities = null,Object? temporaryHandler = freezed,Object? temporaryIndex = freezed,Object? foregrounds = null,Object? selection = freezed,Object? pinned = null,Object? temporaryForegrounds = freezed,Object? toggleableHandlers = null,Object? networkingForegrounds = null,Object? toggleableForegrounds = null,Object? cursor = null,Object? temporaryCursor = freezed,Object? temporaryState = null,Object? lastPosition = freezed,Object? pointers = null,Object? buttons = freezed,Object? location = null,Object? embedding = freezed,Object? saved = null,Object? toolbar = freezed,Object? temporaryToolbar = freezed,Object? rendererStates = null,Object? temporaryRendererStates = freezed,Object? viewOption = null,Object? hideUi = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? isCreating = null,Object? userName = null,Object? penDetected = null,Object? sessionPenOnlyInput = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? index = freezed,Object? handler = null,Object? cameraViewport = null,Object? isSaveDelayed = null,Object? utilities = null,Object? temporaryHandler = freezed,Object? temporaryIndex = freezed,Object? foregrounds = null,Object? selection = freezed,Object? pinned = null,Object? temporaryForegrounds = freezed,Object? toggleableHandlers = null,Object? networkingForegrounds = null,Object? toggleableForegrounds = null,Object? cursor = null,Object? temporaryCursor = freezed,Object? temporaryState = null,Object? lastPosition = freezed,Object? pointers = null,Object? buttons = freezed,Object? location = null,Object? embedding = freezed,Object? saved = null,Object? toolbar = freezed,Object? temporaryToolbar = freezed,Object? rendererStates = null,Object? temporaryRendererStates = freezed,Object? viewOption = null,Object? hideUi = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? isCreating = null,Object? userName = null,Object? penDetected = null,Object? sessionPenOnlyInput = null,}) { return _then(_self.copyWith( index: freezed == index ? _self.index : index // ignore: cast_nullable_to_non_nullable as int?,handler: null == handler ? _self.handler : handler // ignore: cast_nullable_to_non_nullable as Handler,cameraViewport: null == cameraViewport ? _self.cameraViewport : cameraViewport // ignore: cast_nullable_to_non_nullable -as CameraViewport,settingsCubit: null == settingsCubit ? _self.settingsCubit : settingsCubit // ignore: cast_nullable_to_non_nullable -as SettingsCubit,transformCubit: null == transformCubit ? _self.transformCubit : transformCubit // ignore: cast_nullable_to_non_nullable -as TransformCubit,networkingService: null == networkingService ? _self.networkingService : networkingService // ignore: cast_nullable_to_non_nullable -as NetworkingService,isSaveDelayed: null == isSaveDelayed ? _self.isSaveDelayed : isSaveDelayed // ignore: cast_nullable_to_non_nullable +as CameraViewport,isSaveDelayed: null == isSaveDelayed ? _self.isSaveDelayed : isSaveDelayed // ignore: cast_nullable_to_non_nullable as bool,utilities: null == utilities ? _self.utilities : utilities // ignore: cast_nullable_to_non_nullable as UtilitiesState,temporaryHandler: freezed == temporaryHandler ? _self.temporaryHandler : temporaryHandler // ignore: cast_nullable_to_non_nullable as Handler?,temporaryIndex: freezed == temporaryIndex ? _self.temporaryIndex : temporaryIndex // ignore: cast_nullable_to_non_nullable @@ -143,15 +140,12 @@ $ViewOptionCopyWith<$Res> get viewOption { class _CurrentIndex extends CurrentIndex with DiagnosticableTreeMixin { - const _CurrentIndex(this.index, this.handler, this.cameraViewport, this.settingsCubit, this.transformCubit, this.networkingService, {this.isSaveDelayed = false, this.utilities = const UtilitiesState(), this.temporaryHandler, this.temporaryIndex, final List foregrounds = const [], this.selection, this.pinned = false, final List? temporaryForegrounds, final Map> toggleableHandlers = const {}, final List networkingForegrounds = const [], final Map> toggleableForegrounds = const {}, this.cursor = MouseCursor.defer, this.temporaryCursor, this.temporaryState = TemporaryState.allowClick, this.lastPosition, final List pointers = const [], this.buttons, this.location = const AssetLocation(path: ''), this.embedding, this.saved = SaveState.saved, this.toolbar, this.temporaryToolbar, final Map rendererStates = const {}, final Map? temporaryRendererStates = const {}, this.viewOption = const ViewOption(), this.hideUi = HideState.visible, this.areaNavigatorCreate = true, this.areaNavigatorExact = true, this.areaNavigatorAsk = false, this.navigatorEnabled = false, this.navigatorPage = NavigatorPage.waypoints, this.isCreating = false, this.userName = '', this.penDetected = false, this.sessionPenOnlyInput = false}): _foregrounds = foregrounds,_temporaryForegrounds = temporaryForegrounds,_toggleableHandlers = toggleableHandlers,_networkingForegrounds = networkingForegrounds,_toggleableForegrounds = toggleableForegrounds,_pointers = pointers,_rendererStates = rendererStates,_temporaryRendererStates = temporaryRendererStates,super._(); + const _CurrentIndex(this.index, this.handler, this.cameraViewport, {this.isSaveDelayed = false, this.utilities = const UtilitiesState(), this.temporaryHandler, this.temporaryIndex, final List foregrounds = const [], this.selection, this.pinned = false, final List? temporaryForegrounds, final Map> toggleableHandlers = const {}, final List networkingForegrounds = const [], final Map> toggleableForegrounds = const {}, this.cursor = MouseCursor.defer, this.temporaryCursor, this.temporaryState = TemporaryState.allowClick, this.lastPosition, final List pointers = const [], this.buttons, this.location = const AssetLocation(path: ''), this.embedding, this.saved = SaveState.saved, this.toolbar, this.temporaryToolbar, final Map rendererStates = const {}, final Map? temporaryRendererStates = const {}, this.viewOption = const ViewOption(), this.hideUi = HideState.visible, this.areaNavigatorCreate = true, this.areaNavigatorExact = true, this.areaNavigatorAsk = false, this.navigatorEnabled = false, this.navigatorPage = NavigatorPage.waypoints, this.isCreating = false, this.userName = '', this.penDetected = false, this.sessionPenOnlyInput = false}): _foregrounds = foregrounds,_temporaryForegrounds = temporaryForegrounds,_toggleableHandlers = toggleableHandlers,_networkingForegrounds = networkingForegrounds,_toggleableForegrounds = toggleableForegrounds,_pointers = pointers,_rendererStates = rendererStates,_temporaryRendererStates = temporaryRendererStates,super._(); @override final int? index; @override final Handler handler; @override final CameraViewport cameraViewport; -@override final SettingsCubit settingsCubit; -@override final TransformCubit transformCubit; -@override final NetworkingService networkingService; @override@JsonKey() final bool isSaveDelayed; @override@JsonKey() final UtilitiesState utilities; @override final Handler? temporaryHandler; @@ -251,14 +245,14 @@ _$CurrentIndexCopyWith<_CurrentIndex> get copyWith => __$CurrentIndexCopyWithImp void debugFillProperties(DiagnosticPropertiesBuilder properties) { properties ..add(DiagnosticsProperty('type', 'CurrentIndex')) - ..add(DiagnosticsProperty('index', index))..add(DiagnosticsProperty('handler', handler))..add(DiagnosticsProperty('cameraViewport', cameraViewport))..add(DiagnosticsProperty('settingsCubit', settingsCubit))..add(DiagnosticsProperty('transformCubit', transformCubit))..add(DiagnosticsProperty('networkingService', networkingService))..add(DiagnosticsProperty('isSaveDelayed', isSaveDelayed))..add(DiagnosticsProperty('utilities', utilities))..add(DiagnosticsProperty('temporaryHandler', temporaryHandler))..add(DiagnosticsProperty('temporaryIndex', temporaryIndex))..add(DiagnosticsProperty('foregrounds', foregrounds))..add(DiagnosticsProperty('selection', selection))..add(DiagnosticsProperty('pinned', pinned))..add(DiagnosticsProperty('temporaryForegrounds', temporaryForegrounds))..add(DiagnosticsProperty('toggleableHandlers', toggleableHandlers))..add(DiagnosticsProperty('networkingForegrounds', networkingForegrounds))..add(DiagnosticsProperty('toggleableForegrounds', toggleableForegrounds))..add(DiagnosticsProperty('cursor', cursor))..add(DiagnosticsProperty('temporaryCursor', temporaryCursor))..add(DiagnosticsProperty('temporaryState', temporaryState))..add(DiagnosticsProperty('lastPosition', lastPosition))..add(DiagnosticsProperty('pointers', pointers))..add(DiagnosticsProperty('buttons', buttons))..add(DiagnosticsProperty('location', location))..add(DiagnosticsProperty('embedding', embedding))..add(DiagnosticsProperty('saved', saved))..add(DiagnosticsProperty('toolbar', toolbar))..add(DiagnosticsProperty('temporaryToolbar', temporaryToolbar))..add(DiagnosticsProperty('rendererStates', rendererStates))..add(DiagnosticsProperty('temporaryRendererStates', temporaryRendererStates))..add(DiagnosticsProperty('viewOption', viewOption))..add(DiagnosticsProperty('hideUi', hideUi))..add(DiagnosticsProperty('areaNavigatorCreate', areaNavigatorCreate))..add(DiagnosticsProperty('areaNavigatorExact', areaNavigatorExact))..add(DiagnosticsProperty('areaNavigatorAsk', areaNavigatorAsk))..add(DiagnosticsProperty('navigatorEnabled', navigatorEnabled))..add(DiagnosticsProperty('navigatorPage', navigatorPage))..add(DiagnosticsProperty('isCreating', isCreating))..add(DiagnosticsProperty('userName', userName))..add(DiagnosticsProperty('penDetected', penDetected))..add(DiagnosticsProperty('sessionPenOnlyInput', sessionPenOnlyInput)); + ..add(DiagnosticsProperty('index', index))..add(DiagnosticsProperty('handler', handler))..add(DiagnosticsProperty('cameraViewport', cameraViewport))..add(DiagnosticsProperty('isSaveDelayed', isSaveDelayed))..add(DiagnosticsProperty('utilities', utilities))..add(DiagnosticsProperty('temporaryHandler', temporaryHandler))..add(DiagnosticsProperty('temporaryIndex', temporaryIndex))..add(DiagnosticsProperty('foregrounds', foregrounds))..add(DiagnosticsProperty('selection', selection))..add(DiagnosticsProperty('pinned', pinned))..add(DiagnosticsProperty('temporaryForegrounds', temporaryForegrounds))..add(DiagnosticsProperty('toggleableHandlers', toggleableHandlers))..add(DiagnosticsProperty('networkingForegrounds', networkingForegrounds))..add(DiagnosticsProperty('toggleableForegrounds', toggleableForegrounds))..add(DiagnosticsProperty('cursor', cursor))..add(DiagnosticsProperty('temporaryCursor', temporaryCursor))..add(DiagnosticsProperty('temporaryState', temporaryState))..add(DiagnosticsProperty('lastPosition', lastPosition))..add(DiagnosticsProperty('pointers', pointers))..add(DiagnosticsProperty('buttons', buttons))..add(DiagnosticsProperty('location', location))..add(DiagnosticsProperty('embedding', embedding))..add(DiagnosticsProperty('saved', saved))..add(DiagnosticsProperty('toolbar', toolbar))..add(DiagnosticsProperty('temporaryToolbar', temporaryToolbar))..add(DiagnosticsProperty('rendererStates', rendererStates))..add(DiagnosticsProperty('temporaryRendererStates', temporaryRendererStates))..add(DiagnosticsProperty('viewOption', viewOption))..add(DiagnosticsProperty('hideUi', hideUi))..add(DiagnosticsProperty('areaNavigatorCreate', areaNavigatorCreate))..add(DiagnosticsProperty('areaNavigatorExact', areaNavigatorExact))..add(DiagnosticsProperty('areaNavigatorAsk', areaNavigatorAsk))..add(DiagnosticsProperty('navigatorEnabled', navigatorEnabled))..add(DiagnosticsProperty('navigatorPage', navigatorPage))..add(DiagnosticsProperty('isCreating', isCreating))..add(DiagnosticsProperty('userName', userName))..add(DiagnosticsProperty('penDetected', penDetected))..add(DiagnosticsProperty('sessionPenOnlyInput', sessionPenOnlyInput)); } @override String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'CurrentIndex(index: $index, handler: $handler, cameraViewport: $cameraViewport, settingsCubit: $settingsCubit, transformCubit: $transformCubit, networkingService: $networkingService, isSaveDelayed: $isSaveDelayed, utilities: $utilities, temporaryHandler: $temporaryHandler, temporaryIndex: $temporaryIndex, foregrounds: $foregrounds, selection: $selection, pinned: $pinned, temporaryForegrounds: $temporaryForegrounds, toggleableHandlers: $toggleableHandlers, networkingForegrounds: $networkingForegrounds, toggleableForegrounds: $toggleableForegrounds, cursor: $cursor, temporaryCursor: $temporaryCursor, temporaryState: $temporaryState, lastPosition: $lastPosition, pointers: $pointers, buttons: $buttons, location: $location, embedding: $embedding, saved: $saved, toolbar: $toolbar, temporaryToolbar: $temporaryToolbar, rendererStates: $rendererStates, temporaryRendererStates: $temporaryRendererStates, viewOption: $viewOption, hideUi: $hideUi, areaNavigatorCreate: $areaNavigatorCreate, areaNavigatorExact: $areaNavigatorExact, areaNavigatorAsk: $areaNavigatorAsk, navigatorEnabled: $navigatorEnabled, navigatorPage: $navigatorPage, isCreating: $isCreating, userName: $userName, penDetected: $penDetected, sessionPenOnlyInput: $sessionPenOnlyInput)'; + return 'CurrentIndex(index: $index, handler: $handler, cameraViewport: $cameraViewport, isSaveDelayed: $isSaveDelayed, utilities: $utilities, temporaryHandler: $temporaryHandler, temporaryIndex: $temporaryIndex, foregrounds: $foregrounds, selection: $selection, pinned: $pinned, temporaryForegrounds: $temporaryForegrounds, toggleableHandlers: $toggleableHandlers, networkingForegrounds: $networkingForegrounds, toggleableForegrounds: $toggleableForegrounds, cursor: $cursor, temporaryCursor: $temporaryCursor, temporaryState: $temporaryState, lastPosition: $lastPosition, pointers: $pointers, buttons: $buttons, location: $location, embedding: $embedding, saved: $saved, toolbar: $toolbar, temporaryToolbar: $temporaryToolbar, rendererStates: $rendererStates, temporaryRendererStates: $temporaryRendererStates, viewOption: $viewOption, hideUi: $hideUi, areaNavigatorCreate: $areaNavigatorCreate, areaNavigatorExact: $areaNavigatorExact, areaNavigatorAsk: $areaNavigatorAsk, navigatorEnabled: $navigatorEnabled, navigatorPage: $navigatorPage, isCreating: $isCreating, userName: $userName, penDetected: $penDetected, sessionPenOnlyInput: $sessionPenOnlyInput)'; } @@ -269,7 +263,7 @@ abstract mixin class _$CurrentIndexCopyWith<$Res> implements $CurrentIndexCopyWi factory _$CurrentIndexCopyWith(_CurrentIndex value, $Res Function(_CurrentIndex) _then) = __$CurrentIndexCopyWithImpl; @override @useResult $Res call({ - int? index, Handler handler, CameraViewport cameraViewport, SettingsCubit settingsCubit, TransformCubit transformCubit, NetworkingService networkingService, bool isSaveDelayed, UtilitiesState utilities, Handler? temporaryHandler, int? temporaryIndex, List foregrounds, Selection? selection, bool pinned, List? temporaryForegrounds, Map> toggleableHandlers, List networkingForegrounds, Map> toggleableForegrounds, MouseCursor cursor, MouseCursor? temporaryCursor, TemporaryState temporaryState, Offset? lastPosition, List pointers, int? buttons, AssetLocation location, Embedding? embedding, SaveState saved, PreferredSizeWidget? toolbar, PreferredSizeWidget? temporaryToolbar, Map rendererStates, Map? temporaryRendererStates, ViewOption viewOption, HideState hideUi, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, bool navigatorEnabled, NavigatorPage navigatorPage, bool isCreating, String userName, bool penDetected, bool sessionPenOnlyInput + int? index, Handler handler, CameraViewport cameraViewport, bool isSaveDelayed, UtilitiesState utilities, Handler? temporaryHandler, int? temporaryIndex, List foregrounds, Selection? selection, bool pinned, List? temporaryForegrounds, Map> toggleableHandlers, List networkingForegrounds, Map> toggleableForegrounds, MouseCursor cursor, MouseCursor? temporaryCursor, TemporaryState temporaryState, Offset? lastPosition, List pointers, int? buttons, AssetLocation location, Embedding? embedding, SaveState saved, PreferredSizeWidget? toolbar, PreferredSizeWidget? temporaryToolbar, Map rendererStates, Map? temporaryRendererStates, ViewOption viewOption, HideState hideUi, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, bool navigatorEnabled, NavigatorPage navigatorPage, bool isCreating, String userName, bool penDetected, bool sessionPenOnlyInput }); @@ -286,15 +280,12 @@ class __$CurrentIndexCopyWithImpl<$Res> /// Create a copy of CurrentIndex /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? index = freezed,Object? handler = null,Object? cameraViewport = null,Object? settingsCubit = null,Object? transformCubit = null,Object? networkingService = null,Object? isSaveDelayed = null,Object? utilities = null,Object? temporaryHandler = freezed,Object? temporaryIndex = freezed,Object? foregrounds = null,Object? selection = freezed,Object? pinned = null,Object? temporaryForegrounds = freezed,Object? toggleableHandlers = null,Object? networkingForegrounds = null,Object? toggleableForegrounds = null,Object? cursor = null,Object? temporaryCursor = freezed,Object? temporaryState = null,Object? lastPosition = freezed,Object? pointers = null,Object? buttons = freezed,Object? location = null,Object? embedding = freezed,Object? saved = null,Object? toolbar = freezed,Object? temporaryToolbar = freezed,Object? rendererStates = null,Object? temporaryRendererStates = freezed,Object? viewOption = null,Object? hideUi = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? isCreating = null,Object? userName = null,Object? penDetected = null,Object? sessionPenOnlyInput = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? index = freezed,Object? handler = null,Object? cameraViewport = null,Object? isSaveDelayed = null,Object? utilities = null,Object? temporaryHandler = freezed,Object? temporaryIndex = freezed,Object? foregrounds = null,Object? selection = freezed,Object? pinned = null,Object? temporaryForegrounds = freezed,Object? toggleableHandlers = null,Object? networkingForegrounds = null,Object? toggleableForegrounds = null,Object? cursor = null,Object? temporaryCursor = freezed,Object? temporaryState = null,Object? lastPosition = freezed,Object? pointers = null,Object? buttons = freezed,Object? location = null,Object? embedding = freezed,Object? saved = null,Object? toolbar = freezed,Object? temporaryToolbar = freezed,Object? rendererStates = null,Object? temporaryRendererStates = freezed,Object? viewOption = null,Object? hideUi = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? isCreating = null,Object? userName = null,Object? penDetected = null,Object? sessionPenOnlyInput = null,}) { return _then(_CurrentIndex( freezed == index ? _self.index : index // ignore: cast_nullable_to_non_nullable as int?,null == handler ? _self.handler : handler // ignore: cast_nullable_to_non_nullable as Handler,null == cameraViewport ? _self.cameraViewport : cameraViewport // ignore: cast_nullable_to_non_nullable -as CameraViewport,null == settingsCubit ? _self.settingsCubit : settingsCubit // ignore: cast_nullable_to_non_nullable -as SettingsCubit,null == transformCubit ? _self.transformCubit : transformCubit // ignore: cast_nullable_to_non_nullable -as TransformCubit,null == networkingService ? _self.networkingService : networkingService // ignore: cast_nullable_to_non_nullable -as NetworkingService,isSaveDelayed: null == isSaveDelayed ? _self.isSaveDelayed : isSaveDelayed // ignore: cast_nullable_to_non_nullable +as CameraViewport,isSaveDelayed: null == isSaveDelayed ? _self.isSaveDelayed : isSaveDelayed // ignore: cast_nullable_to_non_nullable as bool,utilities: null == utilities ? _self.utilities : utilities // ignore: cast_nullable_to_non_nullable as UtilitiesState,temporaryHandler: freezed == temporaryHandler ? _self.temporaryHandler : temporaryHandler // ignore: cast_nullable_to_non_nullable as Handler?,temporaryIndex: freezed == temporaryIndex ? _self.temporaryIndex : temporaryIndex // ignore: cast_nullable_to_non_nullable diff --git a/app/lib/dialogs/collaboration/dialog.dart b/app/lib/dialogs/collaboration/dialog.dart index 53e8c6affed6..c5175e4c0369 100644 --- a/app/lib/dialogs/collaboration/dialog.dart +++ b/app/lib/dialogs/collaboration/dialog.dart @@ -42,7 +42,7 @@ class CollaborationDialog extends StatelessWidget { @override Widget build(BuildContext context) { final cubit = context.read(); - final service = cubit.state.networkingService; + final service = cubit.networkingService; return BlocBuilder( bloc: service, builder: (context, state) { diff --git a/app/lib/dialogs/export/general.dart b/app/lib/dialogs/export/general.dart index 4d00ace4784f..a3418cbe9efa 100644 --- a/app/lib/dialogs/export/general.dart +++ b/app/lib/dialogs/export/general.dart @@ -280,7 +280,6 @@ class _GeneralExportDialogState extends State { final transform = context .read() .currentIndexCubit - .state .transformCubit .state; diff --git a/app/lib/handlers/eraser.dart b/app/lib/handlers/eraser.dart index 9bad434206ec..bbeec5a77a3c 100644 --- a/app/lib/handlers/eraser.dart +++ b/app/lib/handlers/eraser.dart @@ -95,9 +95,9 @@ class EraserHandler extends Handler { } Future _eraseAt(Offset position, EventContext context) async { - final currentIndex = context.getCurrentIndex(); - final transform = currentIndex.transformCubit.state; - final utilities = currentIndex.utilities; + final cubit = context.getCurrentIndexCubit(); + final transform = cubit.transformCubit.state; + final utilities = cubit.state.utilities; final globalPos = transform.localToGlobal(position); final size = data.strokeWidth; final sizeSquared = size * size; diff --git a/app/lib/handlers/laser.dart b/app/lib/handlers/laser.dart index 2b8e41966bf8..daa7bae2d033 100644 --- a/app/lib/handlers/laser.dart +++ b/app/lib/handlers/laser.dart @@ -132,7 +132,7 @@ class LaserHandler extends Handler with ColoredHandler { final currentIndexCubit = context.read(); final transform = context.read().state; final state = bloc.state as DocumentLoadSuccess; - final penOnlyInput = currentIndexCubit.state.effectivePenOnlyInput; + final penOnlyInput = currentIndexCubit.effectivePenOnlyInput; localPosition = PointerManipulationHandler.calculatePointerPosition( currentIndexCubit.state, localPosition, @@ -176,8 +176,8 @@ class LaserHandler extends Handler with ColoredHandler { changeStartedDrawing(context); _hideCursorWhileDrawing = context.getSettings().hideCursorWhileDrawing; context.refreshForegrounds(); - final currentIndex = context.getCurrentIndex(); - if (currentIndex.moveEnabled && event.kind != PointerDeviceKind.stylus) { + final cubit = context.getCurrentIndexCubit(); + if (cubit.moveEnabled && event.kind != PointerDeviceKind.stylus) { _elements.clear(); return; } diff --git a/app/lib/handlers/pen.dart b/app/lib/handlers/pen.dart index d68598c751ac..805a8050a87b 100644 --- a/app/lib/handlers/pen.dart +++ b/app/lib/handlers/pen.dart @@ -158,7 +158,7 @@ class PenHandler extends Handler with ColoredHandler { if (!bloc.isInBounds(globalPos)) return; final state = bloc.state as DocumentLoadSuccess; final settings = context.read().state; - final penOnlyInput = currentIndexCubit.state.effectivePenOnlyInput; + final penOnlyInput = currentIndexCubit.effectivePenOnlyInput; if (lastPosition[pointer] == localPos) return; lastPosition[pointer] = localPos; if (penOnlyInput && @@ -207,7 +207,7 @@ class PenHandler extends Handler with ColoredHandler { isDrawing = true; changeStartedDrawing(context); _hideCursorWhileDrawing = context.getSettings().hideCursorWhileDrawing; - if (cubit.state.moveEnabled && event.kind != PointerDeviceKind.stylus) { + if (cubit.moveEnabled && event.kind != PointerDeviceKind.stylus) { elements.clear(); context.refreshForegrounds(); return; diff --git a/app/lib/handlers/stamp.dart b/app/lib/handlers/stamp.dart index 58c74055a03e..51baf3bfb411 100644 --- a/app/lib/handlers/stamp.dart +++ b/app/lib/handlers/stamp.dart @@ -10,7 +10,7 @@ class StampHandler extends PastingHandler { final state = context.getState(); if (state == null) return; await _loadComponent( - context.getCurrentIndex().transformCubit, + context.getCurrentIndexCubit().transformCubit, state.data, state.assetService, state.page, diff --git a/app/lib/services/import.dart b/app/lib/services/import.dart index 60b922856d75..fa27315a06ff 100644 --- a/app/lib/services/import.dart +++ b/app/lib/services/import.dart @@ -1183,10 +1183,10 @@ class ImportService { } Future export() async { - final state = _getState(); - if (state == null) return; - final location = state.location; - final fileType = location.fileType; + final bloc = this.bloc; + final state = bloc?.state; + if (state is! DocumentLoadSuccess) return; + final fileType = bloc?.currentIndexCubit.state.location.fileType; final currentIndexCubit = bloc!.currentIndexCubit; final viewport = currentIndexCubit.state.cameraViewport; switch (fileType) { @@ -1204,7 +1204,7 @@ class ImportService { return showDialog( context: context, builder: (context) => BlocProvider.value( - value: bloc!, + value: bloc, child: GeneralExportDialog( options: ImageExportOptions( height: viewport.height?.toDouble() ?? 1000.0, @@ -1220,7 +1220,7 @@ class ImportService { return showDialog( context: context, builder: (context) => BlocProvider.value( - value: bloc!, + value: bloc, child: PdfExportDialog( areas: state.page.areas .map((e) => AreaPreset(name: e.name, area: e)) @@ -1232,7 +1232,7 @@ class ImportService { return showDialog( context: context, builder: (context) => BlocProvider.value( - value: bloc!, + value: bloc, child: GeneralExportDialog( options: SvgExportOptions( width: (viewport.width ?? 1000) / viewport.scale, diff --git a/app/lib/views/app_bar.dart b/app/lib/views/app_bar.dart index 0f0efef936b6..3cb0f2fd833b 100644 --- a/app/lib/views/app_bar.dart +++ b/app/lib/views/app_bar.dart @@ -141,11 +141,10 @@ class _AppBarTitleState extends State<_AppBarTitle> { return BlocBuilder( buildWhen: (previous, current) => previous.location != current.location || + previous.absolute != current.absolute || previous.saved != current.saved || previous.isCreating != current.isCreating || previous.isSaveDelayed != current.isSaveDelayed || - previous.networkingService.isActive != - current.networkingService.isActive || previous.embedding?.save != current.embedding?.save || previous.embedding?.editable != current.embedding?.editable, builder: (context, currentIndex) => @@ -245,213 +244,225 @@ class _AppBarTitleState extends State<_AppBarTitle> { CurrentIndex currentIndex, BuildContext context, ButterflySettings settings, - ) => Row( - textDirection: TextDirection.ltr, - children: [ - Flexible( - child: StreamBuilder( - stream: currentIndex.networkingService.stream, - builder: (context, snapshot) { - return StatefulBuilder( - builder: (context, setState) { - String toFilePath(String name) { - if (state is! DocumentLoaded) return name; - final location = state.location; - return state.fileSystem - .buildDocumentSystem(settings.getRemote(location.remote)) - .convertNameToFileSystem( - name: name, - suffix: '.bfly', - directory: location.parent, - ); - } + ) { + final cubit = context.read(); + return Row( + textDirection: TextDirection.ltr, + children: [ + Flexible( + child: StreamBuilder( + stream: context.read().networkingService.stream, + builder: (context, snapshot) { + return StatefulBuilder( + builder: (context, setState) { + String toFilePath(String name) { + if (state is! DocumentLoaded) return name; + final location = currentIndex.location; + return state.fileSystem + .buildDocumentSystem( + settings.getRemote(location.remote), + ) + .convertNameToFileSystem( + name: name, + suffix: '.bfly', + directory: location.parent, + ); + } - Future submit(String? value) async { - value ??= area == null - ? _nameController.text - : _areaController.text; - if (area == null || areaName == null) { - final cubit = context.read(); - final location = cubit.state.location; - if (state is DocumentLoadSuccess && - currentIndex.isCreating) { - final newLocation = location.copyWith( - path: toFilePath(value), - ); - final savedLocation = await cubit.save( - bloc, - location: newLocation, - force: true, - ); - if (!location.isEmpty && - !savedLocation.isEmpty && - (location.path != savedLocation.path || - location.remote != savedLocation.remote)) { - final documentSystem = state.fileSystem - .buildDocumentSystem( - settings.getRemote(location.remote), - ); - await documentSystem.deleteAsset(location.path); - await cubit.state.settingsCubit.moveAssetReferences( - location, - savedLocation, + Future submit(String? value) async { + value ??= area == null + ? _nameController.text + : _areaController.text; + if (area == null || areaName == null) { + final cubit = context.read(); + final location = cubit.state.location; + if (state is DocumentLoadSuccess && + currentIndex.isCreating) { + final newLocation = location.copyWith( + path: toFilePath(value), ); + final savedLocation = await cubit.save( + bloc, + location: newLocation, + force: true, + ); + if (!location.isEmpty && + !savedLocation.isEmpty && + (location.path != savedLocation.path || + location.remote != savedLocation.remote)) { + final documentSystem = state.fileSystem + .buildDocumentSystem( + settings.getRemote(location.remote), + ); + await documentSystem.deleteAsset(location.path); + await context + .read() + .moveAssetReferences(location, savedLocation); + } } + bloc.add(DocumentDescriptionChanged(name: value)); + } else { + bloc.add( + AreaChanged(areaName, area.copyWith(name: value)), + ); } - bloc.add(DocumentDescriptionChanged(name: value)); - } else { - bloc.add(AreaChanged(areaName, area.copyWith(name: value))); } - } - Widget title = Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - Focus( - onFocusChange: (hasFocus) { - if (!hasFocus) submit(null); - }, - child: TextFormField( - controller: area == null - ? _nameController - : _areaController, - focusNode: area == null - ? _nameFocusNode - : _areaFocusNode, - onFieldSubmitted: submit, - onSaved: submit, - readOnly: currentIndex.embedding?.editable == false, - decoration: InputDecoration( - filled: true, - hintText: AppLocalizations.of(context).untitled, + Widget title = Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Focus( + onFocusChange: (hasFocus) { + if (!hasFocus) submit(null); + }, + child: TextFormField( + controller: area == null + ? _nameController + : _areaController, + focusNode: area == null + ? _nameFocusNode + : _areaFocusNode, + onFieldSubmitted: submit, + onSaved: submit, + readOnly: currentIndex.embedding?.editable == false, + decoration: InputDecoration( + filled: true, + hintText: AppLocalizations.of(context).untitled, + ), ), ), - ), - if (snapshot.data?.connection is NetworkerClient) ...[ - Text( - AppLocalizations.of(context).collaboration, - style: TextTheme.of(context).bodySmall, - ), - ] else - ListenableBuilder( - listenable: _nameController, - builder: (context, child) { - final currentNameFilePath = toFilePath( - _nameController.text, - ); - var showCurrentNameFilePath = - currentIndex.isCreating && - currentNameFilePath != currentIndex.location.path; - if (currentIndex.location.isEmpty && area == null) { - return SizedBox(); - } - return Tooltip( - message: currentIndex.location.identifier, - child: Text( - showCurrentNameFilePath - ? currentNameFilePath - : ((currentIndex.absolute && - currentIndex + if (snapshot.data?.connection is NetworkerClient) ...[ + Text( + AppLocalizations.of(context).collaboration, + style: TextTheme.of(context).bodySmall, + ), + ] else + ListenableBuilder( + listenable: Listenable.merge([ + _nameController, + _nameFocusNode, + ]), + builder: (context, child) { + final currentNameFilePath = toFilePath( + _nameController.text, + ); + var showCurrentNameFilePath = + area == null && + _nameFocusNode.hasFocus && + currentNameFilePath != + currentIndex.location.path; + if (currentIndex.location.isEmpty && + area == null && + !showCurrentNameFilePath) { + return SizedBox(); + } + return Tooltip( + message: currentIndex.location.identifier, + child: Text( + showCurrentNameFilePath + ? currentNameFilePath + : ((currentIndex.absolute && + currentIndex + .location + .path + .isEmpty) + ? currentIndex.location.fileType + ?.getLocalizedName(context) + : currentIndex .location - .path - .isEmpty) - ? currentIndex.location.fileType - ?.getLocalizedName(context) - : currentIndex - .location - .pathWithoutLeadingSlash) ?? - AppLocalizations.of(context).document, - style: TextTheme.of(context).bodySmall?.copyWith( - fontStyle: showCurrentNameFilePath - ? FontStyle.italic - : FontStyle.normal, + .pathWithoutLeadingSlash) ?? + AppLocalizations.of(context).document, + style: TextTheme.of(context).bodySmall + ?.copyWith( + fontStyle: showCurrentNameFilePath + ? FontStyle.italic + : FontStyle.normal, + ), ), - ), - ); - }, - ), - ], - ); - return title; - }, - ); - }, + ); + }, + ), + ], + ); + return title; + }, + ); + }, + ), ), - ), - const SizedBox(width: 8), - if (state is DocumentLoadSuccess) ...[ - if ((!state.hasAutosave( - currentIndex.networkingService, - currentIndex.embedding, - ) || - settings.showSaveButton) && - currentIndex.embedding?.save != false) - SizedBox( - width: 42, - child: Builder( - builder: (context) { - Widget icon = PhosphorIcon(switch (currentIndex.saved) { - SaveState.saving => PhosphorIconsLight.download, - _ when currentIndex.isSaveDelayed => PhosphorIconsLight.clock, - SaveState.saved => PhosphorIconsFill.floppyDisk, - SaveState.unsaved || - SaveState.absoluteRead => PhosphorIconsLight.floppyDisk, - }); - String tooltip = switch (currentIndex.saved) { - SaveState.saving => AppLocalizations.of(context).saving, - _ when currentIndex.isSaveDelayed => AppLocalizations.of( - context, - ).saveDelayed, - SaveState.saved => AppLocalizations.of(context).saved, - SaveState.unsaved => AppLocalizations.of(context).unsaved, - SaveState.absoluteRead => AppLocalizations.of( - context, - ).readOnly, - }; - return IconButton( - icon: icon, - tooltip: tooltip, - onPressed: () { - Actions.maybeInvoke(context, SaveIntent()); - }, - ); + const SizedBox(width: 8), + if (state is DocumentLoadSuccess) ...[ + if ((!cubit.hasAutosave() || settings.showSaveButton) && + currentIndex.embedding?.save != false) + SizedBox( + width: 42, + child: Builder( + builder: (context) { + Widget icon = PhosphorIcon(switch (currentIndex.saved) { + SaveState.saving => PhosphorIconsLight.download, + _ when currentIndex.isSaveDelayed => + PhosphorIconsLight.clock, + SaveState.saved => PhosphorIconsFill.floppyDisk, + SaveState.unsaved || + SaveState.absoluteRead => PhosphorIconsLight.floppyDisk, + }); + String tooltip = switch (currentIndex.saved) { + SaveState.saving => AppLocalizations.of(context).saving, + _ when currentIndex.isSaveDelayed => AppLocalizations.of( + context, + ).saveDelayed, + SaveState.saved => AppLocalizations.of(context).saved, + SaveState.unsaved => AppLocalizations.of(context).unsaved, + SaveState.absoluteRead => AppLocalizations.of( + context, + ).readOnly, + }; + return IconButton( + icon: icon, + tooltip: tooltip, + onPressed: () { + Actions.maybeInvoke(context, SaveIntent()); + }, + ); + }, + ), + ), + if (state.currentAreaName.isNotEmpty) + IconButton( + icon: const PhosphorIcon(PhosphorIconsLight.signOut), + tooltip: AppLocalizations.of(context).exitArea, + onPressed: () { + context.read().add(const CurrentAreaChanged('')); }, ), - ), - if (state.currentAreaName.isNotEmpty) - IconButton( - icon: const PhosphorIcon(PhosphorIconsLight.signOut), - tooltip: AppLocalizations.of(context).exitArea, - onPressed: () { - context.read().add(const CurrentAreaChanged('')); - }, - ), - if (state.absolute) - IconButton( - icon: PhosphorIcon( - state.location.fileType.icon(PhosphorIconsStyle.light), + if (state.absolute) + IconButton( + icon: PhosphorIcon( + currentIndex.location.fileType.icon(PhosphorIconsStyle.light), + ), + tooltip: AppLocalizations.of(context).export, + onPressed: () => context.read().export(), ), - tooltip: AppLocalizations.of(context).export, - onPressed: () => context.read().export(), - ), - SearchButton(controller: widget.searchController), - if (state.location.path != '' && currentIndex.embedding == null) ...[ - IconButton( - icon: const PhosphorIcon(PhosphorIconsLight.folder), - onPressed: () { - Actions.maybeInvoke( - context, - ChangePathIntent(), - ); - }, - tooltip: AppLocalizations.of(context).changeDocumentPath, - ), + SearchButton(controller: widget.searchController), + if (currentIndex.location.path != '' && + currentIndex.embedding == null) ...[ + IconButton( + icon: const PhosphorIcon(PhosphorIconsLight.folder), + onPressed: () { + Actions.maybeInvoke( + context, + ChangePathIntent(), + ); + }, + tooltip: AppLocalizations.of(context).changeDocumentPath, + ), + ], ], ], - ], - ); + ); + } } class MainPopupMenu extends StatelessWidget { @@ -483,7 +494,6 @@ class MainPopupMenu extends StatelessWidget { buildWhen: (previous, current) => previous.embedding != current.embedding || previous.hideUi != current.hideUi || - previous.networkingService != current.networkingService || previous.saved != current.saved, builder: (context, state) { final size = MediaQuery.sizeOf(context); @@ -783,7 +793,9 @@ class MainPopupMenu extends StatelessWidget { if (state.embedding == null && settings.hasFlag('collaboration')) BlocBuilder( - bloc: state.networkingService, + bloc: context + .read() + .networkingService, builder: (_, state) { final isOpen = state?.connection.isOpen ?? false; return MenuItemButton( diff --git a/app/lib/views/navigator/files.dart b/app/lib/views/navigator/files.dart index 2d55435712c3..7834099a61a7 100644 --- a/app/lib/views/navigator/files.dart +++ b/app/lib/views/navigator/files.dart @@ -1,96 +1,92 @@ -import 'package:butterfly/api/open.dart'; -import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/settings.dart'; -import 'package:butterfly/embed/embedding.dart'; -import 'package:butterfly/views/files/view.dart'; -import 'package:butterfly/views/main.dart'; -import 'package:butterfly_api/butterfly_api.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:lw_file_system/lw_file_system.dart'; - -class FilesNavigatorPage extends StatefulWidget { - const FilesNavigatorPage({super.key}); - - @override - State createState() => _FilesNavigatorPageState(); -} - -class _FilesNavigatorPageState extends State { - ExternalStorage? _remote; - (NoteData, AssetLocation)? _opened; - - @override - void initState() { - super.initState(); - _remote = context.read().getRemote(); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => - (previous is DocumentLoadSuccess && - current is! DocumentLoadSuccess) || - (previous is! DocumentLoadSuccess && - current is DocumentLoadSuccess) || - (current is DocumentLoaded && - previous is DocumentLoaded && - current.location != previous.location), - builder: (context, state) { - AssetLocation? location; - if (state is DocumentLoaded) { - location = state.location; - location = AssetLocation( - remote: location.remote, - path: '/${location.path}', - ); - } - if (_opened != null) { - return ProjectPage( - embedding: Embedding( - editable: false, - save: false, - internal: true, - location: _opened?.$2, - onOpen: () async { - final bloc = context.read(); - await bloc.save(); - openFile(context, true, _opened!.$2, _opened!.$1); - }, - onExit: () => setState(() { - _opened = null; - }), - ), - data: _opened?.$1, - ); - } - return SingleChildScrollView( - child: FilesView( - remote: _remote, - activeAsset: location, - initialPath: location?.parent, - onTap: (value) async { - final bloc = context.read(); - await bloc.save(); - openFile(context, true, value.location); - }, - onPreview: (value) { - final data = value.data!.load(); - if (data == null) return; - setState(() { - _opened = (data, value.location); - }); - }, - onRemoteChanged: (remote) { - setState(() { - _remote = remote; - }); - }, - collapsed: true, - ), - ); - }, - ); - } -} +import 'package:butterfly/api/open.dart'; +import 'package:butterfly/bloc/document_bloc.dart'; +import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/settings.dart'; +import 'package:butterfly/embed/embedding.dart'; +import 'package:butterfly/views/files/view.dart'; +import 'package:butterfly/views/main.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:lw_file_system/lw_file_system.dart'; + +class FilesNavigatorPage extends StatefulWidget { + const FilesNavigatorPage({super.key}); + + @override + State createState() => _FilesNavigatorPageState(); +} + +class _FilesNavigatorPageState extends State { + ExternalStorage? _remote; + (NoteData, AssetLocation)? _opened; + + @override + void initState() { + super.initState(); + _remote = context.read().getRemote(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => + previous.location != current.location || + previous.absolute != current.absolute, + builder: (context, state) { + AssetLocation? location; + if (state is DocumentLoaded) { + location = state.location; + location = AssetLocation( + remote: location.remote, + path: '/${location.path}', + ); + } + if (_opened != null) { + return ProjectPage( + embedding: Embedding( + editable: false, + save: false, + internal: true, + location: _opened?.$2, + onOpen: () async { + final bloc = context.read(); + await bloc.save(); + openFile(context, true, _opened!.$2, _opened!.$1); + }, + onExit: () => setState(() { + _opened = null; + }), + ), + data: _opened?.$1, + ); + } + return SingleChildScrollView( + child: FilesView( + remote: _remote, + activeAsset: location, + initialPath: location?.parent, + onTap: (value) async { + final bloc = context.read(); + await bloc.save(); + openFile(context, true, value.location); + }, + onPreview: (value) { + final data = value.data!.load(); + if (data == null) return; + setState(() { + _opened = (data, value.location); + }); + }, + onRemoteChanged: (remote) { + setState(() { + _remote = remote; + }); + }, + collapsed: true, + ), + ); + }, + ); + } +} diff --git a/app/lib/views/pen_only_toggle.dart b/app/lib/views/pen_only_toggle.dart index 72e8fa86f21f..f5698edc4c46 100644 --- a/app/lib/views/pen_only_toggle.dart +++ b/app/lib/views/pen_only_toggle.dart @@ -40,7 +40,9 @@ class PenOnlyToggle extends StatelessWidget { } // Use effective pen-only state (considers both setting and session) - final penOnlyEnabled = currentIndex.effectivePenOnlyInput; + final penOnlyEnabled = context + .read() + .effectivePenOnlyInput; final isAutoMode = settings.penOnlyInput == null; return Tooltip( diff --git a/app/lib/views/view.dart b/app/lib/views/view.dart index efb2728f304b..0b4d0517978d 100644 --- a/app/lib/views/view.dart +++ b/app/lib/views/view.dart @@ -68,9 +68,9 @@ class _MainViewViewportState extends State bool _isMousePenOrTouch(PointerDeviceKind kind) => _isMouseOrPen(kind) || kind == PointerDeviceKind.touch; - bool _isTouchMoveGesture(CurrentIndex currentIndex) => + bool _isTouchMoveGesture(CurrentIndexCubit currentIndex) => currentIndex.moveEnabled && - currentIndex.pointers.every( + currentIndex.state.pointers.every( (pointer) => _pointerKinds[pointer] == PointerDeviceKind.touch, ); @@ -335,7 +335,7 @@ class _MainViewViewportState extends State return; } final currentIndexState = cubit.state; - if (_isTouchMoveGesture(currentIndexState)) { + if (_isTouchMoveGesture(cubit)) { if (currentIndexState.pointers.isEmpty) { return; } @@ -632,11 +632,7 @@ class _MainViewViewportState extends State cubit.move( -details.focalPointDelta / sensitivity / - cubit - .state - .transformCubit - .state - .size, + cubit.transformCubit.state.size, currentArea: state.currentArea, ); } else { @@ -675,11 +671,7 @@ class _MainViewViewportState extends State cubit.slide( details.velocity.pixelsPerSecond / sensitivity / - cubit - .state - .transformCubit - .state - .size, + cubit.transformCubit.state.size, details.scaleVelocity, currentArea: state.currentArea, ); @@ -697,7 +689,7 @@ class _MainViewViewportState extends State }, onScaleStart: (details) { _isScalingDisabled ??= !_isTouchMoveGesture( - cubit.state, + cubit, ); _ruler = RulerHandler.getInteractiveRuler( currentIndex, diff --git a/app/pubspec.lock b/app/pubspec.lock index 252c3a55f13e..9a5bbb8786ff 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -252,10 +252,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "62ffa266d9a23b79fb3fcbc206afc00bb979417ba57b1324c546b5aab95ba057" + sha256: cad0e811a289ea2a941119dc483c204ec1684cbb9a8fc7351fe4a230b8313160 url: "https://pub.dev" source: hosted - version: "7.1.1" + version: "7.2.0" connectivity_plus_platform_interface: dependency: transitive description: @@ -901,10 +901,10 @@ packages: dependency: "direct main" description: name: network_info_plus - sha256: f424bad71994a1dc8594b00a6f71a665f7714ee7b32d397656d592c030d8555f + sha256: "4a1217d16644ed59f88e415e2777a4c81f4da5bffb724566825e5551c6379567" url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.2.0" network_info_plus_platform_interface: dependency: transitive description: @@ -985,10 +985,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "4bf625947f6c7713ee242296a682e23e44823c09cf9d79e4f1238923c92db852" + sha256: f5c435dc0e0d461e5b32471a870f769b6a1cc46930637efe24fbc535314e78ad url: "https://pub.dev" source: hosted - version: "10.1.0" + version: "10.2.0" package_info_plus_platform_interface: dependency: transitive description: @@ -1282,10 +1282,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: a857d8b1479250aff6b57a51b2c02d31ca05848d441817c43f1640c885c286c0 + sha256: "9eee8283462d91a7a1c8bdb67d08874abd75a2f8fae3bc0ca033035e375fb3d8" url: "https://pub.dev" source: hosted - version: "13.1.0" + version: "13.2.0" share_plus_platform_interface: dependency: transitive description: @@ -1726,4 +1726,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.12.2 <4.0.0" - flutter: "3.44.3" + flutter: "3.44.4" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 87362f70bf51..8954403ef774 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -17,7 +17,7 @@ version: 2.6.0-beta.1+187 environment: sdk: ">=3.12.2 <4.0.0" - flutter: 3.44.3 + flutter: 3.44.4 dependencies: flutter: diff --git a/app/test/bloc/document_bloc_test.dart b/app/test/bloc/document_bloc_test.dart index f5a77cc1c59a..793d8fe8849e 100644 --- a/app/test/bloc/document_bloc_test.dart +++ b/app/test/bloc/document_bloc_test.dart @@ -502,7 +502,6 @@ void main() { fileSystem: fileSystem, windowCubit: windowCubit, assetService: assetService, - location: currentIndexCubit.state.location, absolute: currentIndexCubit.state.absolute, ); @@ -853,7 +852,7 @@ void main() { ); final viewport = currentIndexCubit.state.cameraViewport; - final transform = currentIndexCubit.state.transformCubit.state; + final transform = currentIndexCubit.transformCubit.state; final left = (viewport.x - transform.position.dx) * transform.size; final top = (viewport.y - transform.position.dy) * transform.size; final right = left + viewport.width!; diff --git a/metadata/en-US/changelogs/187.txt b/metadata/en-US/changelogs/187.txt index 6ebdffd9d18e..fa54ece8419f 100644 --- a/metadata/en-US/changelogs/187.txt +++ b/metadata/en-US/changelogs/187.txt @@ -1,6 +1,8 @@ * Add combine paths option ([#1071](https://github.com/LinwoodDev/Butterfly/issues/1071)) * Add xournal++ exporter * Improve xournal++ importer +* Improve state management for better linking different systems together * Fix refresh foregrounds can be run concurrently +* Fix location synchronization issues Read more here: https://linwood.dev/butterfly/2.6.0-beta.1 \ No newline at end of file From 3a267a686f9017fb427c2dcefe37c51e7af6efcd Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Fri, 26 Jun 2026 17:53:14 +0200 Subject: [PATCH 013/117] Add next and previous page shortcuts --- app/lib/actions/next_page.dart | 35 +++++++++++++++ app/lib/actions/previous_page.dart | 35 +++++++++++++++ app/lib/actions/shortcuts.dart | 63 +++++++++++++++------------ app/lib/l10n/app_en.arb | 4 +- app/lib/settings/inputs/keyboard.dart | 2 + app/lib/settings/inputs/shortcut.dart | 2 + app/lib/views/main.dart | 2 + metadata/en-US/changelogs/187.txt | 1 + 8 files changed, 114 insertions(+), 30 deletions(-) create mode 100644 app/lib/actions/next_page.dart create mode 100644 app/lib/actions/previous_page.dart diff --git a/app/lib/actions/next_page.dart b/app/lib/actions/next_page.dart new file mode 100644 index 000000000000..b3713b8f6414 --- /dev/null +++ b/app/lib/actions/next_page.dart @@ -0,0 +1,35 @@ +import 'package:butterfly/bloc/document_bloc.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:keybinder/keybinder.dart'; + +class NextPageIntent extends Intent { + const NextPageIntent(); +} + +const nextPageShortcut = ShortcutDefinition( + id: 'next_page', + intent: NextPageIntent(), + defaultActivator: SingleActivator(LogicalKeyboardKey.pageDown), +); + +class NextPageAction extends Action { + final BuildContext context; + + NextPageAction(this.context); + + @override + void invoke(NextPageIntent intent) { + final bloc = context.read(); + final state = bloc.state; + if (state is! DocumentLoadSuccess) return; + + final pages = state.data.getPages(true); + final index = pages.indexOf(state.pageName); + if (index < 0 || index >= pages.length - 1) return; + + bloc.add(PageChanged(pages[index + 1])); + } +} diff --git a/app/lib/actions/previous_page.dart b/app/lib/actions/previous_page.dart new file mode 100644 index 000000000000..370d3b6961ca --- /dev/null +++ b/app/lib/actions/previous_page.dart @@ -0,0 +1,35 @@ +import 'package:butterfly/bloc/document_bloc.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:keybinder/keybinder.dart'; + +class PreviousPageIntent extends Intent { + const PreviousPageIntent(); +} + +const previousPageShortcut = ShortcutDefinition( + id: 'previous_page', + intent: PreviousPageIntent(), + defaultActivator: SingleActivator(LogicalKeyboardKey.pageUp), +); + +class PreviousPageAction extends Action { + final BuildContext context; + + PreviousPageAction(this.context); + + @override + void invoke(PreviousPageIntent intent) { + final bloc = context.read(); + final state = bloc.state; + if (state is! DocumentLoadSuccess) return; + + final pages = state.data.getPages(true); + final index = pages.indexOf(state.pageName); + if (index <= 0) return; + + bloc.add(PageChanged(pages[index - 1])); + } +} diff --git a/app/lib/actions/shortcuts.dart b/app/lib/actions/shortcuts.dart index b6da2842f544..7786f3f55399 100644 --- a/app/lib/actions/shortcuts.dart +++ b/app/lib/actions/shortcuts.dart @@ -1,3 +1,5 @@ +import 'package:butterfly/actions/next_page.dart'; +import 'package:butterfly/actions/previous_page.dart'; import 'package:keybinder/keybinder.dart'; import 'package:butterfly/actions/background.dart'; import 'package:butterfly/actions/change_path.dart'; @@ -34,10 +36,12 @@ export 'package:butterfly/actions/hide_ui.dart'; export 'package:butterfly/actions/image_export.dart'; export 'package:butterfly/actions/new.dart'; export 'package:butterfly/actions/next.dart'; +export 'package:butterfly/actions/next_page.dart'; export 'package:butterfly/actions/packs.dart'; export 'package:butterfly/actions/paste.dart'; export 'package:butterfly/actions/pdf_export.dart'; export 'package:butterfly/actions/previous.dart'; +export 'package:butterfly/actions/previous_page.dart'; export 'package:butterfly/actions/redo.dart'; export 'package:butterfly/actions/save.dart'; export 'package:butterfly/actions/select.dart'; @@ -59,37 +63,36 @@ extension ShortcutDefinitionLocalization on ShortcutDefinition { return AppLocalizations.of(context).toolNumber(num + 1); } } + final loc = AppLocalizations.of(context); return switch (this) { - newShortcut => AppLocalizations.of(context).newNote, - newFromTemplateShortcut => AppLocalizations.of( - context, - ).newFromTemplateShortcut, - exportShortcut => AppLocalizations.of(context).export, - exportTextShortcut => AppLocalizations.of(context).exportAsTextShortcut, - imageExportShortcut => AppLocalizations.of(context).exportAsImageShortcut, - pdfExportShortcut => AppLocalizations.of(context).exportAsPdfShortcut, - svgExportShortcut => AppLocalizations.of(context).exportAsSvgShortcut, - packsShortcut => AppLocalizations.of(context).packs, - settingsShortcut => AppLocalizations.of(context).settings, - exitShortcut => AppLocalizations.of(context).exit, - searchShortcut => AppLocalizations.of(context).search, - undoShortcut => AppLocalizations.of(context).undo, - redoShortcut => AppLocalizations.of(context).redo, - backgroundShortcut => AppLocalizations.of(context).background, - saveShortcut => AppLocalizations.of(context).save, - changePathShortcut => AppLocalizations.of(context).changePathShortcut, - zoomInShortcut => AppLocalizations.of(context).zoomIn, - zoomOutShortcut => AppLocalizations.of(context).zoomOut, - fullScreenShortcut => AppLocalizations.of(context).fullScreenShortcut, - hideUIShortcut => AppLocalizations.of(context).hideUI, - nextShortcut => AppLocalizations.of(context).nextSlide, - previousShortcut => AppLocalizations.of(context).previousSlide, - togglePresentationShortcut => AppLocalizations.of( - context, - ).pausePresentation, - selectAllShortcut => AppLocalizations.of(context).selectAll, - pasteShortcut => AppLocalizations.of(context).paste, + newShortcut => loc.newNote, + newFromTemplateShortcut => loc.newFromTemplateShortcut, + exportShortcut => loc.export, + exportTextShortcut => loc.exportAsTextShortcut, + imageExportShortcut => loc.exportAsImageShortcut, + pdfExportShortcut => loc.exportAsPdfShortcut, + svgExportShortcut => loc.exportAsSvgShortcut, + packsShortcut => loc.packs, + settingsShortcut => loc.settings, + exitShortcut => loc.exit, + searchShortcut => loc.search, + undoShortcut => loc.undo, + redoShortcut => loc.redo, + backgroundShortcut => loc.background, + saveShortcut => loc.save, + changePathShortcut => loc.changePathShortcut, + zoomInShortcut => loc.zoomIn, + zoomOutShortcut => loc.zoomOut, + fullScreenShortcut => loc.fullScreenShortcut, + hideUIShortcut => loc.hideUI, + nextShortcut => loc.nextSlide, + previousShortcut => loc.previousSlide, + nextPageShortcut => loc.nextPage, + previousPageShortcut => loc.previousPage, + togglePresentationShortcut => loc.pausePresentation, + selectAllShortcut => loc.selectAll, + pasteShortcut => loc.paste, _ => id, }; } @@ -108,6 +111,8 @@ final keybinder = Keybinder( hideUIShortcut, nextShortcut, previousShortcut, + nextPageShortcut, + previousPageShortcut, togglePresentationShortcut, selectAllShortcut, searchShortcut, diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index f084edfd8dc5..2d7548bd558f 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -1368,5 +1368,7 @@ "holdShortcutsDescription": "Hold a key to temporarily switch to another tool. Releasing the key switches back to the previous tool.", "key": "Key", "bringMovedElementsToFront": "Bring moved elements to front", - "addTool": "Add tool" + "addTool": "Add tool", + "nextPage": "Next page", + "previousPage": "Previous page" } diff --git a/app/lib/settings/inputs/keyboard.dart b/app/lib/settings/inputs/keyboard.dart index 8a8074ea4378..840bf2a93f02 100644 --- a/app/lib/settings/inputs/keyboard.dart +++ b/app/lib/settings/inputs/keyboard.dart @@ -43,6 +43,8 @@ class KeyboardInputSettings extends StatelessWidget { hideUIShortcut, nextShortcut, previousShortcut, + nextPageShortcut, + previousPageShortcut, togglePresentationShortcut, selectAllShortcut, pasteShortcut, diff --git a/app/lib/settings/inputs/shortcut.dart b/app/lib/settings/inputs/shortcut.dart index 711c626d52a3..0ea7887acac5 100644 --- a/app/lib/settings/inputs/shortcut.dart +++ b/app/lib/settings/inputs/shortcut.dart @@ -17,6 +17,8 @@ List<(String?, String)> getInputShortcutOptions(BuildContext context) { zoomOutShortcut, fullScreenShortcut, hideUIShortcut, + nextPageShortcut, + previousPageShortcut, selectAllShortcut, pasteShortcut, ...changeToolShortcuts, diff --git a/app/lib/views/main.dart b/app/lib/views/main.dart index caa5fffc54c9..b83fdf800681 100644 --- a/app/lib/views/main.dart +++ b/app/lib/views/main.dart @@ -601,6 +601,8 @@ class _ProjectPageState extends State { HideUIIntent: HideUIAction(context), NextIntent: NextAction(context), PreviousIntent: PreviousAction(context), + NextPageIntent: NextPageAction(context), + PreviousPageIntent: PreviousPageAction(context), TogglePresentationIntent: TogglePresentationAction(context), PasteIntent: PasteAction(context), SelectAllIntent: SelectAllAction(context), diff --git a/metadata/en-US/changelogs/187.txt b/metadata/en-US/changelogs/187.txt index fa54ece8419f..1cbb678860cc 100644 --- a/metadata/en-US/changelogs/187.txt +++ b/metadata/en-US/changelogs/187.txt @@ -1,5 +1,6 @@ * Add combine paths option ([#1071](https://github.com/LinwoodDev/Butterfly/issues/1071)) * Add xournal++ exporter +* Add next and previous page shortcuts * Improve xournal++ importer * Improve state management for better linking different systems together * Fix refresh foregrounds can be run concurrently From 7c970d648470282f31b1f0077d6a4e5a06dd1039 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Fri, 26 Jun 2026 20:33:37 +0200 Subject: [PATCH 014/117] Unify area context menu and area selection context menu, related to #1151 --- app/lib/dialogs/area/context.dart | 109 +++++---- app/lib/views/navigator/areas.dart | 34 +-- app/lib/widgets/context_menu.dart | 378 ++++++++++++++--------------- metadata/en-US/changelogs/187.txt | 1 + 4 files changed, 252 insertions(+), 270 deletions(-) diff --git a/app/lib/dialogs/area/context.dart b/app/lib/dialogs/area/context.dart index 4a1a85a576ed..246e0c368c72 100644 --- a/app/lib/dialogs/area/context.dart +++ b/app/lib/dialogs/area/context.dart @@ -1,4 +1,5 @@ import 'package:butterfly/bloc/document_bloc.dart'; +import 'package:butterfly/cubits/current_index.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/dialogs/layers.dart'; import 'package:butterfly/dialogs/pages.dart'; @@ -20,46 +21,50 @@ ContextMenuBuilder buildAreaContextMenu( DocumentBloc bloc, DocumentLoadSuccess state, Area area, - SettingsCubit settingsCubit, -) => (context) { + SettingsCubit settingsCubit, { + bool pop = true, + bool includeRenameAndEnterArea = true, +}) => (context) { final cubit = bloc.currentIndexCubit; return [ - ContextMenuItem( - icon: const PhosphorIcon(PhosphorIconsLight.textT), - label: AppLocalizations.of(context).rename, - onPressed: () async { - Navigator.of(context).pop(); - final name = await showDialog( - context: context, - builder: (context) => NameDialog( - value: area.name, - validator: defaultNameValidator( - context, - state.page.getAreaNames().toList(), + if (includeRenameAndEnterArea) ...[ + ContextMenuItem( + icon: const PhosphorIcon(PhosphorIconsLight.textT), + label: AppLocalizations.of(context).rename, + onPressed: () async { + if (pop) Navigator.of(context).pop(); + final name = await showDialog( + context: context, + builder: (context) => NameDialog( + value: area.name, + validator: defaultNameValidator( + context, + state.page.getAreaNames().toList(), + ), + button: AppLocalizations.of(context).rename, ), - button: AppLocalizations.of(context).rename, - ), - ); - if (name == null) return; - bloc.add(AreaChanged(area.name, area.copyWith(name: name))); - }, - ), - ContextMenuItem( - icon: area.name == state.currentAreaName - ? const PhosphorIcon(PhosphorIconsLight.signOut) - : const PhosphorIcon(PhosphorIconsLight.signIn), - label: area.name == state.currentAreaName - ? AppLocalizations.of(context).exitArea - : AppLocalizations.of(context).enterArea, - onPressed: () { - Navigator.of(context).pop(); - bloc.add( - CurrentAreaChanged( - area.name == state.currentAreaName ? '' : area.name, - ), - ); - }, - ), + ); + if (name == null) return; + bloc.add(AreaChanged(area.name, area.copyWith(name: name))); + }, + ), + ContextMenuItem( + icon: area.name == state.currentAreaName + ? const PhosphorIcon(PhosphorIconsLight.signOut) + : const PhosphorIcon(PhosphorIconsLight.signIn), + label: area.name == state.currentAreaName + ? AppLocalizations.of(context).exitArea + : AppLocalizations.of(context).enterArea, + onPressed: () { + if (pop) Navigator.of(context).pop(); + bloc.add( + CurrentAreaChanged( + area.name == state.currentAreaName ? '' : area.name, + ), + ); + }, + ), + ], ContextMenuItem( icon: const PhosphorIcon(PhosphorIconsLight.copySimple), label: AppLocalizations.of(context).duplicate, @@ -75,7 +80,7 @@ ContextMenuBuilder buildAreaContextMenu( ); if (selectedPages == null) return; if (!context.mounted) return; - Navigator.of(context).pop(); + if (pop) Navigator.of(context).pop(); bloc.add(AreasDuplicated(area, selectedPages)); }, ), @@ -83,13 +88,13 @@ ContextMenuBuilder buildAreaContextMenu( icon: const PhosphorIcon(PhosphorIconsLight.trash), label: AppLocalizations.of(context).delete, onPressed: () { - Navigator.of(context).pop(); + if (pop) Navigator.of(context).pop(); bloc.add(AreasRemoved([area.name])); }, ), ContextMenuItem( onPressed: () { - Navigator.of(context).pop(true); + if (pop) Navigator.of(context).pop(true); cubit.changeSelection(area); }, icon: const PhosphorIcon(PhosphorIconsLight.faders), @@ -99,21 +104,23 @@ ContextMenuBuilder buildAreaContextMenu( bloc, area, settingsCubit, - cubit.renderers - .where((e) => e.area == area) - .map( - (e) => e.transform( - position: -area.position.toOffset(), - relative: true, - ), - ) - .map((e) => e?.element) - .nonNulls - .toList(), + _getAreaElements(cubit, area), + pop: pop, )(context), ]; }; +List _getAreaElements(CurrentIndexCubit cubit, Area area) { + return cubit.renderers + .where((e) => e.area == area) + .map( + (e) => e.transform(position: -area.position.toOffset(), relative: true), + ) + .map((e) => e?.element) + .nonNulls + .toList(); +} + ContextMenuBuilder buildGeneralAreaContextMenu( DocumentBloc bloc, Area area, diff --git a/app/lib/views/navigator/areas.dart b/app/lib/views/navigator/areas.dart index b439841f3894..f6ba0f244fc6 100644 --- a/app/lib/views/navigator/areas.dart +++ b/app/lib/views/navigator/areas.dart @@ -5,7 +5,6 @@ import 'package:butterfly/dialogs/area/context.dart'; import 'package:butterfly/dialogs/area/init.dart'; import 'package:butterfly/handlers/handler.dart'; import 'package:butterfly/helpers/page.dart'; -import 'package:butterfly/helpers/point.dart'; import 'package:butterfly/helpers/rect.dart'; import 'package:butterfly/models/viewport.dart'; import 'package:butterfly/widgets/context_menu.dart'; @@ -93,7 +92,7 @@ class _AreasViewState extends State { Widget buildAreaTile( DocumentBloc bloc, CameraViewport viewport, - DocumentLoaded state, + DocumentLoadSuccess state, Rect viewportRect, Area? current, Area area, { @@ -172,39 +171,14 @@ class _AreasViewState extends State { actions: isSelectionMode ? null : [ - ...buildGeneralAreaContextMenu( + ...buildAreaContextMenu( bloc, + state, area, context.read(), - context - .read() - .renderers - .where((e) => e.area == area) - .map( - (e) => e.transform( - position: -area.position.toOffset(), - relative: true, - ), - ) - .map((e) => e?.element) - .nonNulls - .toList(), pop: false, + includeRenameAndEnterArea: false, )(context).map((e) => buildMenuItem(context, e, false, false)), - MenuItemButton( - leadingIcon: const PhosphorIcon(PhosphorIconsLight.trash), - onPressed: () async { - final result = await showDialog( - context: context, - builder: (context) => const DeleteDialog(), - ); - if (result != true) return; - if (context.mounted) { - bloc.add(AreasRemoved([area.name])); - } - }, - child: Text(AppLocalizations.of(context).delete), - ), ], ); } diff --git a/app/lib/widgets/context_menu.dart b/app/lib/widgets/context_menu.dart index 1fb37fc511ae..191bb68f66e9 100644 --- a/app/lib/widgets/context_menu.dart +++ b/app/lib/widgets/context_menu.dart @@ -1,189 +1,189 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:animations/animations.dart'; -import 'package:butterfly/cubits/settings.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:material_leap/material_leap.dart'; -import 'package:phosphor_flutter/phosphor_flutter.dart'; - -typedef ContextMenuBuilder = - List Function(BuildContext context); - -sealed class ContextMenuEntry { - final String label; - final Widget icon; - final MenuSerializableShortcut? shortcut; - - ContextMenuEntry({required this.label, required this.icon, this.shortcut}); -} - -class ContextMenuItem extends ContextMenuEntry { - final VoidCallback? onPressed; - - ContextMenuItem({ - required super.label, - required super.icon, - this.onPressed, - super.shortcut, - }); -} - -class ContextMenuGroup extends ContextMenuEntry { - final List children; - - ContextMenuGroup({ - required super.label, - required super.icon, - required this.children, - super.shortcut, - }); -} - -class ContextMenu extends StatefulWidget { - final Offset position; - final ContextMenuBuilder builder; - final double maxWidth, maxHeight; - - const ContextMenu({ - super.key, - this.position = Offset.zero, - required this.builder, - this.maxHeight = 300, - this.maxWidth = 300, - }); - - @override - State createState() => _ContextMenuState(); -} - -class _ContextMenuState extends State - with TickerProviderStateMixin { - late AnimationController _controller; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - duration: const Duration(milliseconds: 200), - vsync: this, - ); - _animation = CurvedAnimation( - parent: _controller, - curve: Curves.fastOutSlowIn, - ); - _controller.forward(); - } - - late Animation _animation; - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final isMobile = context.read().state.platformTheme.isMobile( - context, - ); - final entries = widget.builder(context); - return CustomSingleChildLayout( - delegate: DesktopTextSelectionToolbarLayoutDelegate( - anchor: widget.position, - ), - child: SlideTransition( - position: Tween( - begin: const Offset(0, -.5), - end: Offset.zero, - ).animate(_animation), - transformHitTests: false, - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: max(widget.maxWidth, isMobile ? double.infinity : 60), - maxHeight: min(widget.maxHeight, isMobile ? 60 : double.infinity), - ), - child: Material( - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: ListView( - scrollDirection: isMobile ? Axis.horizontal : Axis.vertical, - shrinkWrap: true, - children: entries - .map((entry) => buildMenuItem(context, entry, isMobile, true)) - .toList(), - ), - ), - ), - ), - ); - } -} - -Widget buildMenuItem( - BuildContext context, - ContextMenuEntry entry, - bool isIcon, - bool showCaret, -) { - Widget buildItemWidget(VoidCallback? onPressed) => AspectRatio( - aspectRatio: 1, - child: IconButton( - icon: entry.icon, - tooltip: entry.label, - onPressed: onPressed, - iconSize: 30, - ), - ); - return switch (entry) { - ContextMenuItem() => - isIcon - ? buildItemWidget(entry.onPressed) - : MenuItemButton( - leadingIcon: entry.icon, - onPressed: entry.onPressed, - child: Text(entry.label), - ), - ContextMenuGroup() => - isIcon - ? MenuAnchor( - menuChildren: entry.children, - builder: (context, controller, child) => - buildItemWidget(controller.toggle), - ) - : SubmenuButton( - menuChildren: entry.children, - leadingIcon: entry.icon, - trailingIcon: showCaret - ? const PhosphorIcon(PhosphorIconsLight.caretRight) - : null, - menuStyle: const MenuStyle(alignment: Alignment.bottomRight), - child: Text(entry.label), - ), - }; -} - -Future showContextMenu({ - required BuildContext context, - Offset position = Offset.zero, - required ContextMenuBuilder builder, - double maxHeight = 400, - double maxWidth = 300, -}) async { - final RenderBox box = context.findRenderObject() as RenderBox; - final Offset globalPos = box.localToGlobal(position); - AdaptiveTextSelectionToolbar; - return showModal( - context: context, - useRootNavigator: true, - builder: (context) { - return ContextMenu( - position: globalPos, - builder: builder, - maxHeight: maxHeight, - maxWidth: maxWidth, - ); - }, - ); -} +import 'dart:async'; +import 'dart:math'; + +import 'package:animations/animations.dart'; +import 'package:butterfly/cubits/settings.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:material_leap/material_leap.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +typedef ContextMenuBuilder = + List Function(BuildContext context); + +sealed class ContextMenuEntry { + final String label; + final Widget icon; + final MenuSerializableShortcut? shortcut; + + ContextMenuEntry({required this.label, required this.icon, this.shortcut}); +} + +class ContextMenuItem extends ContextMenuEntry { + final VoidCallback? onPressed; + + ContextMenuItem({ + required super.label, + required super.icon, + this.onPressed, + super.shortcut, + }); +} + +class ContextMenuGroup extends ContextMenuEntry { + final List children; + + ContextMenuGroup({ + required super.label, + required super.icon, + required this.children, + super.shortcut, + }); +} + +class ContextMenu extends StatefulWidget { + final Offset position; + final ContextMenuBuilder builder; + final double maxWidth, maxHeight; + + const ContextMenu({ + super.key, + this.position = Offset.zero, + required this.builder, + this.maxHeight = 300, + this.maxWidth = 300, + }); + + @override + State createState() => _ContextMenuState(); +} + +class _ContextMenuState extends State + with TickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + _animation = CurvedAnimation( + parent: _controller, + curve: Curves.fastOutSlowIn, + ); + _controller.forward(); + } + + late Animation _animation; + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isMobile = context.read().state.platformTheme.isMobile( + context, + ); + final entries = widget.builder(context); + return CustomSingleChildLayout( + delegate: DesktopTextSelectionToolbarLayoutDelegate( + anchor: widget.position, + ), + child: SlideTransition( + position: Tween( + begin: const Offset(0, -.5), + end: Offset.zero, + ).animate(_animation), + transformHitTests: false, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: max(widget.maxWidth, isMobile ? double.infinity : 60), + maxHeight: min(widget.maxHeight, isMobile ? 60 : double.infinity), + ), + child: Material( + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: ListView( + scrollDirection: isMobile ? Axis.horizontal : Axis.vertical, + shrinkWrap: true, + children: entries + .map((entry) => buildMenuItem(context, entry, isMobile, true)) + .toList(), + ), + ), + ), + ), + ); + } +} + +Widget buildMenuItem( + BuildContext context, + ContextMenuEntry entry, + bool isIcon, + bool showCaret, +) { + Widget buildItemWidget(VoidCallback? onPressed) => AspectRatio( + aspectRatio: 1, + child: IconButton( + icon: entry.icon, + tooltip: entry.label, + onPressed: onPressed, + iconSize: 30, + ), + ); + return switch (entry) { + ContextMenuItem() => + isIcon + ? buildItemWidget(entry.onPressed) + : MenuItemButton( + leadingIcon: entry.icon, + onPressed: entry.onPressed, + child: Text(entry.label), + ), + ContextMenuGroup() => + isIcon + ? MenuAnchor( + menuChildren: entry.children, + builder: (context, controller, child) => + buildItemWidget(controller.toggle), + ) + : SubmenuButton( + menuChildren: entry.children, + leadingIcon: entry.icon, + trailingIcon: showCaret + ? const PhosphorIcon(PhosphorIconsLight.caretRight) + : null, + menuStyle: const MenuStyle(alignment: Alignment.bottomRight), + child: Text(entry.label), + ), + }; +} + +Future showContextMenu({ + required BuildContext context, + Offset position = Offset.zero, + required ContextMenuBuilder builder, + double maxHeight = 400, + double maxWidth = 300, +}) async { + final RenderBox box = context.findRenderObject() as RenderBox; + final Offset globalPos = box.localToGlobal(position); + AdaptiveTextSelectionToolbar; + return showModal( + context: context, + useRootNavigator: true, + builder: (context) { + return ContextMenu( + position: globalPos, + builder: builder, + maxHeight: maxHeight, + maxWidth: maxWidth, + ); + }, + ); +} diff --git a/metadata/en-US/changelogs/187.txt b/metadata/en-US/changelogs/187.txt index 1cbb678860cc..c6b7ae570476 100644 --- a/metadata/en-US/changelogs/187.txt +++ b/metadata/en-US/changelogs/187.txt @@ -3,6 +3,7 @@ * Add next and previous page shortcuts * Improve xournal++ importer * Improve state management for better linking different systems together +* Unify area context menu and area selection context menu ([#1151](https://github.com/LinwoodDev/Butterfly/issues/1151)) * Fix refresh foregrounds can be run concurrently * Fix location synchronization issues From 6ee6a09e3a312e4c97a6c1018b09b878e9c2713a Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sat, 27 Jun 2026 15:02:31 +0200 Subject: [PATCH 015/117] Add onenote docs --- docs/astro.config.mjs | 4 + docs/package.json | 4 +- docs/pnpm-lock.yaml | 688 +++++++++++------------ docs/src/content/docs/docs/v2/onenote.md | 290 ++++++++++ 4 files changed, 640 insertions(+), 346 deletions(-) create mode 100644 docs/src/content/docs/docs/v2/onenote.md diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 45a49d5a664b..8945376c7e10 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -129,6 +129,10 @@ export default defineConfig({ ...getSidebarTranslatedLabel("Migrating"), link: "/docs/v2/migrating/", }, + { + label: "OneNote", + link: "/docs/v2/onenote/", + }, { ...getSidebarTranslatedLabel("Tools"), items: [ diff --git a/docs/package.json b/docs/package.json index 3de6ed9fefcd..20732e295172 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,12 +13,12 @@ "@astrojs/check": "^0.9.9", "@astrojs/markdown-satteri": "^0.3.2", "@astrojs/react": "^6.0.0", - "@astrojs/starlight": "^0.41.0", + "@astrojs/starlight": "^0.41.1", "@linwooddev/style": "github:LinwoodDev/style#efbdf8f05d4ef1cf6fec65c90194d30b68b3c64e&path:/packages/web", "@phosphor-icons/react": "^2.1.10", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", - "astro": "^7.0.2", + "astro": "^7.0.3", "katex": "^0.17.0", "react": "^19.2.7", "react-dom": "^19.2.7", diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index 6c04519681ac..df201d902897 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@astrojs/check': specifier: ^0.9.9 - version: 0.9.9(prettier@3.8.4)(typescript@6.0.3) + version: 0.9.9(prettier@3.8.5)(typescript@6.0.3) '@astrojs/markdown-satteri': specifier: ^0.3.2 version: 0.3.2 @@ -18,8 +18,8 @@ importers: specifier: ^6.0.0 version: 6.0.0(@types/node@24.13.2)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) '@astrojs/starlight': - specifier: ^0.41.0 - version: 0.41.0(@astrojs/markdown-remark@7.2.0)(astro@7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3) + specifier: ^0.41.1 + version: 0.41.1(@astrojs/markdown-remark@7.2.0)(astro@7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3) '@linwooddev/style': specifier: github:LinwoodDev/style#efbdf8f05d4ef1cf6fec65c90194d30b68b3c64e&path:/packages/web version: https://codeload.github.com/LinwoodDev/style/tar.gz/efbdf8f05d4ef1cf6fec65c90194d30b68b3c64e#path:/packages/web @@ -33,8 +33,8 @@ importers: specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.17) astro: - specifier: ^7.0.2 - version: 7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + specifier: ^7.0.3 + version: 7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) katex: specifier: ^0.17.0 version: 0.17.0 @@ -50,7 +50,7 @@ importers: devDependencies: '@vite-pwa/astro': specifier: ^1.2.0 - version: 1.2.0(astro@7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1)) + version: 1.2.0(astro@7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1)) sass: specifier: ^1.101.0 version: 1.101.0 @@ -59,7 +59,7 @@ importers: version: 0.35.2 vite-plugin-pwa: specifier: ^1.3.0 - version: 1.3.0(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1) + version: 1.3.0(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1) workbox-window: specifier: ^7.4.1 version: 7.4.1 @@ -78,69 +78,69 @@ packages: peerDependencies: typescript: ^5.0.0 || ^6.0.0 - '@astrojs/compiler-binding-darwin-arm64@0.2.2': - resolution: {integrity: sha512-1WxpECx3izz5X4Ha3l6ex79HZlUHpKBTElGJsfOosnhVvXhccfcXAsBlrYPNmUOKJWiG7mcrje0ELSr1KPE69Q==} + '@astrojs/compiler-binding-darwin-arm64@0.2.3': + resolution: {integrity: sha512-sJIHeL1ONXEBLob8ZaXfmX6iCftUno08G/cMXj2FJnL0xNbHuELcEq1mjxHVFHNgUYu4P7xJNm2mpc0zUEPoKw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@astrojs/compiler-binding-darwin-x64@0.2.2': - resolution: {integrity: sha512-PdIQidwQ4nUX/qNL0JXzSwNYadr+yX/Yoo+kZSCxBZqlAqug9SlKR/1H5EG5jSEpkEDRo2d1JdrO1W4kU6uNHw==} + '@astrojs/compiler-binding-darwin-x64@0.2.3': + resolution: {integrity: sha512-P0NYu6aaIeLCqFfszxxBHL0a5WRaYigNVbDoO654Gi5Q2au5duDb5xZBv5EqUg4qnQVC173FXNvGZu1M7nk+/w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@astrojs/compiler-binding-linux-arm64-gnu@0.2.2': - resolution: {integrity: sha512-5sZicPkJCoyxTEOW2he+u6UV97pTXY4c7fbHOZ/aslu9ewsunD0231QUBSvnjBB9hRFzzP2JRfNCLY+IvWfw0Q==} + '@astrojs/compiler-binding-linux-arm64-gnu@0.2.3': + resolution: {integrity: sha512-PqVN5AqhuDqfx3ejaerwrC8codpV9jnyKV+IOel027qsJ1anFUJLdjUlY8VVys0xgd8lmqveX11OkcaQj/otTg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@astrojs/compiler-binding-linux-arm64-musl@0.2.2': - resolution: {integrity: sha512-Vto5fqRzMepQNJPeEhQLOIkmAoNbSBQNlpMe64OHTjEbAtQpJYVHEwe+8WJLaEGLA9qBksLv7ctiYPLLxgVVYQ==} + '@astrojs/compiler-binding-linux-arm64-musl@0.2.3': + resolution: {integrity: sha512-O3e2CbN4yTsRguWYNnRd0p5YQ0H3fb7KpcR0W4R319q/gq5B1pJ7eqNbiO3b8g2AuiEcRTiUz5jeGT9j69cxOQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@astrojs/compiler-binding-linux-x64-gnu@0.2.2': - resolution: {integrity: sha512-NJTTaUDU49WEJT+ImS9evlv3i3x42HN8PbcqdyydI9picnKtuf5TxRL94naoW09YDMYWpkrwVeVqaG7GTSIepQ==} + '@astrojs/compiler-binding-linux-x64-gnu@0.2.3': + resolution: {integrity: sha512-hbLBjXVp+96psMe7/7uqyrquGiULXANrq6REVxxPK/I5VzebZ7LHmSfykmByUbLyR1u+K6CTBKgvdQsK2L+2Xw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@astrojs/compiler-binding-linux-x64-musl@0.2.2': - resolution: {integrity: sha512-9nFdNDkWaU35bhWUIciDmi439W0fq2ZNgv1GCi2A/LSb+8vTw9l9z+fVW4YEn7Tlvbs8gyQkJvbc62ZD1H76xA==} + '@astrojs/compiler-binding-linux-x64-musl@0.2.3': + resolution: {integrity: sha512-vIiEvOwrJfHZMaTmqUCrFTIwMYL0+PD3Rvy7kFDQgERyx3zhaw8CPa01MCCqa+/sj344BGrXKZ6ti37SgNLMhw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@astrojs/compiler-binding-wasm32-wasi@0.2.2': - resolution: {integrity: sha512-3NWaxRc3KSwr9zHBEGcQ147rMgAhi/HzZWZJPRN2YLbtuLhoeBPruhdX1MIlOYAg3FvJPYdzGCAKvvWWU6pF0A==} + '@astrojs/compiler-binding-wasm32-wasi@0.2.3': + resolution: {integrity: sha512-p9S2X8z/mUR2SMzAVJRFMCt8YaalKR+pjl2DgpdjzCQc6ww4bo8kiy54tgKqxZeNF5c+/2tCDTQIxVSm9V1FsA==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@astrojs/compiler-binding-win32-arm64-msvc@0.2.2': - resolution: {integrity: sha512-QsyOgocLGOwDm8zAyuAiziALMzbTTevaqBLv5lvdhyhj2JC5yjp0H3yeEBtE0owRLr1gDQdX0+1qBSc5tTwp4g==} + '@astrojs/compiler-binding-win32-arm64-msvc@0.2.3': + resolution: {integrity: sha512-vcCG6JttIb5vbSmcxO2O398hpVj7lQ349iS7cjgYP6ZuLVEnw+9qPAr2MM2kJkU5wEGZqJ2gyi/M7UJoPwH1iQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@astrojs/compiler-binding-win32-x64-msvc@0.2.2': - resolution: {integrity: sha512-+/C8Oh9YS8C0fwtaqDtUSPniKwlJqgiQbxp7XuzxCP0RI/Jf7yOlz9ZHNr6jD3ZJa4CHdq0mYq7pd8QFiNGIZA==} + '@astrojs/compiler-binding-win32-x64-msvc@0.2.3': + resolution: {integrity: sha512-hKssjNvC36e00Inb1GW1JsVyCFSCGnIjKem4S8q0VIW6cpWAUpvYB4qQU2HIDGD6SDX0ork4F5sWkNWkp2hrGQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@astrojs/compiler-binding@0.2.2': - resolution: {integrity: sha512-PkCo+UcSxMt9pufjv28kRy8YU88O/XNgm7apwkyhkrCecLY62QCj5UA3nOTMO88PPyVKnUh/6FZbPefBhDD5IA==} + '@astrojs/compiler-binding@0.2.3': + resolution: {integrity: sha512-Xz3iBNse+hXXD25IXxsuXEt2ai8klAWE15CRm/EQBc9+aE3jXaF07DZx+iakk3HC6NHvWlEPzLPyxsLgPzOJsw==} engines: {node: ^20.19.0 || >=22.12.0} - '@astrojs/compiler-rs@0.2.2': - resolution: {integrity: sha512-C0qoz7Hxa2krlwMYfzQ1ZxOjR6cGmO+iskjRXXCdUM85W/cAPGTi/MSQlLkv0ADMz+Go1/lqeRBjL8AZwsFffQ==} + '@astrojs/compiler-rs@0.2.3': + resolution: {integrity: sha512-JRAtRcPxS4JeAZEIQFQ6GecBs/Wyp4m6/E8vBNxSgVfo1AtRVLUqRCl5oCGOZ0X/BSBB3Vef/7IlzyiGKi2ORA==} '@astrojs/compiler@2.13.1': resolution: {integrity: sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg==} @@ -192,8 +192,8 @@ packages: '@astrojs/sitemap@3.7.3': resolution: {integrity: sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA==} - '@astrojs/starlight@0.41.0': - resolution: {integrity: sha512-5QQLMArxdnLOQhKbXTEU4wh4hydUilXHKG6llOfIZuggM3Mr4b6RvdvInYtqWLfeAZxnlvIHGrwurC3wWW9zRg==} + '@astrojs/starlight@0.41.1': + resolution: {integrity: sha512-avf2OmrVg6GdVU18juebjjIIuLa+uS3syHuJ/3yDaEFP/8it+YvcxRrYDSf7K6rC4v770UxIddba2hAqQyTeYA==} peerDependencies: '@astrojs/markdown-remark': ^7.2.0 astro: ^7.0.2 @@ -721,29 +721,52 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} - '@bruits/satteri-darwin-arm64@0.9.1': - resolution: {integrity: sha512-NE4qC2sRd0+R+oPMsKcUikIvWTGpAV16fSXNBoMSW7nK7Pd9e/ZGB7M8knep83pFmGmOb/5NbGKiVIh3oLbljw==} + '@bruits/satteri-darwin-arm64@0.9.3': + resolution: {integrity: sha512-dRUZZrdwh1asfTOyM1nDNmzolhnHtlIFpqYrl1Tdd3YVcaebKmrfJgGL7NAoGPjbEwYmZxaugrxA0uzw83c0dw==} cpu: [arm64] os: [darwin] - '@bruits/satteri-darwin-x64@0.9.1': - resolution: {integrity: sha512-Am8z5nX0L/sJR/n7np+5rMVP434MOBe3zlU+IO8fXbv2UaPNRCrEQPMS6lyxCj7dZHQI2AynSJw3v4fgQE8xSQ==} + '@bruits/satteri-darwin-x64@0.9.3': + resolution: {integrity: sha512-wgNCTRp2hPSpNMGFv5A4+6+VXgRJIlBZ7XKb3iwjV8YjRWNIjzE5zV2fUeYynyZYVRkuJ9aYFqQmWhc1e5H+UQ==} cpu: [x64] os: [darwin] - '@bruits/satteri-linux-x64-gnu@0.9.1': - resolution: {integrity: sha512-Bivw60+SIfmlaU9wEZ39HAcyPh1xU9TvOrz4KJgc+ziRStQs4EzYoWW4Eh9aSdiol+xV0vjEWYuA79aF3dr7kg==} + '@bruits/satteri-linux-arm64-gnu@0.9.3': + resolution: {integrity: sha512-A/pWy8Jb/PhDYc2/JFuYh06gFJcsfBUBDl81YydGYBrL/Z4nItDfhNDNOibyeSN/lKKDRlycIHEIajjErk00sQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@bruits/satteri-linux-arm64-musl@0.9.3': + resolution: {integrity: sha512-L6YxmyOSickzo4pE5WmZfNTJnjX0MtgKOsuwQfNZECTx9Ir5vl2B37EIwnxe2AybuPPHl+FqVQtthNDUdH4Vgg==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@bruits/satteri-linux-x64-gnu@0.9.3': + resolution: {integrity: sha512-RgH6GPihg9Lzs2yHUsMjqiLxfLyOdmBty8sg9pBY9B4CBnvdOzvg8vklqN+C4qrEEdA9TwpbDpHr1AshLKyRpw==} cpu: [x64] os: [linux] libc: [glibc] - '@bruits/satteri-wasm32-wasi@0.9.1': - resolution: {integrity: sha512-lEFk5ebh5SxRquA5RJisEoMoDmOiSnS10PGgXgXeVlWgF8KWKI9D3hSzVqNjEg/qKOjHcNdjIfyx/03Wrl6J3g==} + '@bruits/satteri-linux-x64-musl@0.9.3': + resolution: {integrity: sha512-BeWhVORjNTIomePznUKiMbHZTqC0j7sMXZFsISmbX+po5d33KLkqBqKh6K332CHJ8KUmCWx16FfPjwsoysttQg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@bruits/satteri-wasm32-wasi@0.9.3': + resolution: {integrity: sha512-dFNcOHKWV2cztCPnYTn7kZ9D7kNOt8N239z5ysFkNHLxJrfK7zaKIXQbfXYN32C+JoVFqAcTIOeWH2+VnsCOHg==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@bruits/satteri-win32-x64-msvc@0.9.1': - resolution: {integrity: sha512-YBhm8yARopswAPeTih11BzMxWVQFD5CI9Yryp6HWepi6F/CeMycqxv18DXrj9U2fDDlbVGwT2IiEM2UPBjIPDQ==} + '@bruits/satteri-win32-arm64-msvc@0.9.3': + resolution: {integrity: sha512-VnwjBHiAra/PNNEza8eSZdQiG4A3PtTJJwUDtOPAc6iTs0BWZwZX8+OPUZE7//yQCBhgvEMcI8vpwsAwCb6qGQ==} + cpu: [arm64] + os: [win32] + + '@bruits/satteri-win32-x64-msvc@0.9.3': + resolution: {integrity: sha512-Dsoe4reWe69MyILmMwU6iISIceTW7YIFqbyym7haf9DhUvqkYfMAyp7GMM21JzV0SpG9A2BwzFVP7iq9mmxrpA==} cpu: [x64] os: [win32] @@ -784,26 +807,14 @@ packages: '@emmetio/stream-reader@2.2.0': resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==} - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - - '@emnapi/core@1.9.1': - resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} - - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - '@emnapi/runtime@1.9.1': - resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} - - '@emnapi/wasi-threads@1.2.0': - resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} - - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} @@ -961,17 +972,17 @@ packages: cpu: [x64] os: [win32] - '@expressive-code/core@0.43.1': - resolution: {integrity: sha512-H4rUJXKyS6y2q9Ig9bIp3dFhWhkZQIeH/jRGl3DROlslrGvfD4OC9qzmvKEFExm+/DtdvvHMQ8/Olmrcfxp+wQ==} + '@expressive-code/core@0.44.0': + resolution: {integrity: sha512-xgiF2P6tYUbrhi3+x0S8xHZWT1t3Bvb3U91tAtRbLb9HLejLvYc5GZUqKICKLaUN4iSGhhNJu2fM/aH8e5yCMg==} - '@expressive-code/plugin-frames@0.43.1': - resolution: {integrity: sha512-tENfLw2UDeq5h749tTLvUtQYvgjIiQc6W7PBCR5xQ4yuE/QftManKJfUQjwJo6RRsAimVQDN4alhFTJ3aq1Khg==} + '@expressive-code/plugin-frames@0.44.0': + resolution: {integrity: sha512-V6M6+zVc1GzqCvXkQHc2m5rcFOIVzJgMq5gnfrMnVf2gwtj/sg4H93c1f/mGeqHycubwkHFUDyParAOiGeDZeA==} - '@expressive-code/plugin-shiki@0.43.1': - resolution: {integrity: sha512-NdceinYEROXODNgB/ix+7oCdIg+nGyok+E+p2lU9YlWd1xKshXdXpmmptKfkuU27MJ5jjnfhMCI78YYBGi9GtQ==} + '@expressive-code/plugin-shiki@0.44.0': + resolution: {integrity: sha512-RZsdaqlbGqyAQKuoX4myQXxjmiE2l5KBpJ/gKPh62tCdIdpWyjbzVqSo8+5XsezZxkfi8AJ/J6EUaBTPROFX/Q==} - '@expressive-code/plugin-text-markers@0.43.1': - resolution: {integrity: sha512-JWf8wdbZSNoGY4TFv3lmt3/NNDaCP7iYL6rRYD05g8YYjKL62hKUHLl5+B47+v0+bqbuMhXDN7qz2wywFUvMkg==} + '@expressive-code/plugin-text-markers@0.44.0': + resolution: {integrity: sha512-0/m3A5b+lz2upyNq+wzZ1S69HRoJmyFs5LsR42lVZ9pmGRlBiSBYQpvqlji4DBj1+Riamxc0AvcCr5kuzOQeWA==} '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} @@ -1310,8 +1321,8 @@ packages: '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} - '@napi-rs/wasm-runtime@1.1.5': - resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 @@ -1319,8 +1330,8 @@ packages: '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} - '@oxc-project/types@0.133.0': - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@oxc-project/types@0.137.0': + resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} '@pagefind/darwin-arm64@1.5.2': resolution: {integrity: sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==} @@ -1455,97 +1466,97 @@ packages: react: '>= 16.8' react-dom: '>= 16.8' - '@rolldown/binding-android-arm64@1.0.3': - resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + '@rolldown/binding-android-arm64@1.1.3': + resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.3': - resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + '@rolldown/binding-darwin-arm64@1.1.3': + resolution: {integrity: sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.3': - resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + '@rolldown/binding-darwin-x64@1.1.3': + resolution: {integrity: sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.3': - resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + '@rolldown/binding-freebsd-x64@1.1.3': + resolution: {integrity: sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': + resolution: {integrity: sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.3': - resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + '@rolldown/binding-linux-arm64-gnu@1.1.3': + resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.3': - resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + '@rolldown/binding-linux-arm64-musl@1.1.3': + resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + '@rolldown/binding-linux-ppc64-gnu@1.1.3': + resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.3': - resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + '@rolldown/binding-linux-s390x-gnu@1.1.3': + resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.3': - resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + '@rolldown/binding-linux-x64-gnu@1.1.3': + resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.3': - resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + '@rolldown/binding-linux-x64-musl@1.1.3': + resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.3': - resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + '@rolldown/binding-openharmony-arm64@1.1.3': + resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.3': - resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + '@rolldown/binding-wasm32-wasi@1.1.3': + resolution: {integrity: sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.3': - resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + '@rolldown/binding-win32-arm64-msvc@1.1.3': + resolution: {integrity: sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.3': - resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + '@rolldown/binding-win32-x64-msvc@1.1.3': + resolution: {integrity: sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1605,32 +1616,32 @@ packages: rollup: optional: true - '@shikijs/core@4.2.0': - resolution: {integrity: sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ==} + '@shikijs/core@4.3.0': + resolution: {integrity: sha512-EooU3i9F6IAE8kEu+AnGf9DFZWkQBZ+hJn3tLVbsH+61mtQiva5biai66fAA6nvFPXkLgvrh7BrR7YcJU83xQQ==} engines: {node: '>=20'} - '@shikijs/engine-javascript@4.2.0': - resolution: {integrity: sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og==} + '@shikijs/engine-javascript@4.3.0': + resolution: {integrity: sha512-hTv/KiFf2tpiqlACPiztGGurEARWIutB8YUhcrA1pUC7VzzwKO+g5crUocrLztrZ5ro5Z4hbXg7bYclETn3gSQ==} engines: {node: '>=20'} - '@shikijs/engine-oniguruma@4.2.0': - resolution: {integrity: sha512-hTorK1dffPkpbMUk6Z+828PgRo7d07HbnizoP0hNPFjhxMHctj0Px/qoHeGMYafc6ju+u9iMldN4JbVzNQM++g==} + '@shikijs/engine-oniguruma@4.3.0': + resolution: {integrity: sha512-1vMdN3gHfnKfLYwecUI2ITJI4RhHt96xEaJumVn7Heb0IlJ8WQMIH0Voak+2j22BpSNKdnOfB/pCTPnPm2gq7A==} engines: {node: '>=20'} - '@shikijs/langs@4.2.0': - resolution: {integrity: sha512-bwrVRlJ0wUhZxAbVdvBbv2TTC9yLsh4C/IO5Ofz0T8MQntgDvyVnkbjw9vi50r1kx7RCIJdnJnjZAwmAsXFLZQ==} + '@shikijs/langs@4.3.0': + resolution: {integrity: sha512-rnlqFbBRSys9bT4gl/5rw9RnS0W/I84ZldXPkO7cvlEMoV85TyF/aU01N7/NbSR776RNLjrJKjfFUXJR6wN1Cg==} engines: {node: '>=20'} - '@shikijs/primitive@4.2.0': - resolution: {integrity: sha512-NOq+DtUkVBJtZMVXL5A0vI0Xk8nvDYaXetFHSJFlOqjDZIVhIPRYFdGkSoElDqNuegikcc3A76SNUa8dTqtAYA==} + '@shikijs/primitive@4.3.0': + resolution: {integrity: sha512-CPkz64PTa5diRW1ggzMZH9VM/du4RNChYgVtgqrFcgruvIybmCvySv8GkiHSczUHXYuuR8TdKEwFx+UnZMpgdg==} engines: {node: '>=20'} - '@shikijs/themes@4.2.0': - resolution: {integrity: sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w==} + '@shikijs/themes@4.3.0': + resolution: {integrity: sha512-Avgt05YiT+Y3prjIc9lmQxhJzHBcCfR6cjiFW4OyaMBbt2A6trX5rfjUzx+Vj/mE9qpArYjatnqo9XPjQNW/AQ==} engines: {node: '>=20'} - '@shikijs/types@4.2.0': - resolution: {integrity: sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw==} + '@shikijs/types@4.3.0': + resolution: {integrity: sha512-oc8b9U2SYvofKZk8e/737nIX0qwf6eV2vHFATeObAu7r+mUVpLs8Re0BmVkIjAWAYgkmG/CzLNo7rzuBzRu/wQ==} engines: {node: '>=20'} '@shikijs/vscode-textmate@10.0.2': @@ -1817,13 +1828,13 @@ packages: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true - astro-expressive-code@0.43.1: - resolution: {integrity: sha512-xddgwQxFRwpnnAnU7kSfrO82SsOAq7sQrYpXxVcrN9k/0aqNlTH2+mLrOMm1wXm6jdFKepst3hd8/qWojwuunw==} + astro-expressive-code@0.44.0: + resolution: {integrity: sha512-b1wN/ZvbJprzxlGKIpIes2kQrCY5KRLwys2tWbZAZyjGZcW5ZtgneZnBwzNRiBna9/48d4mQl19KLjcRuhO1hw==} peerDependencies: - astro: ^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta + astro: ^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0 - astro@7.0.2: - resolution: {integrity: sha512-Rj31HS85pVSfiQTxKTwH6vTmF8y57iRfkgb1uiCTtdLIh3jlUKPAPXtEmHXRKp/PdTS00D4EXYlWXypY+AVVCA==} + astro@7.0.3: + resolution: {integrity: sha512-CK+G+Tl2DMV1EXCwVG45vyurxf2IfRTklMxDhRKn+tst9Yl8rWXpudL62Fa6zin5Bt968FBvuyASj1aJShROZg==} engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true peerDependencies: @@ -1872,8 +1883,8 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.10.38: - resolution: {integrity: sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==} + baseline-browser-mapping@2.10.40: + resolution: {integrity: sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==} engines: {node: '>=6.0.0'} hasBin: true @@ -2125,8 +2136,8 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.5.377: - resolution: {integrity: sha512-cH1jZgJHoezfTnKfKwnScpHywTFVnJUNITDPREFdhNjiuD502+QFpG0Qk7G8jhsV/f+CEAFlIrzP1fT+IMb92g==} + electron-to-chromium@1.5.379: + resolution: {integrity: sha512-v/qV5aV5EUA2pGilzUCq5/eyOloZAqDZBu9UMBIzgPpLlprjSR6zswsWBTv0KpqxLGUAZEwhO95ZCt7srymNVA==} emmet@2.4.11: resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} @@ -2169,8 +2180,8 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - es-to-primitive@1.3.1: - resolution: {integrity: sha512-CxN9N56HYfd2m/acc/NOFrZQsN9kU4eh+2kk6A707Kz1krH8tKmfrs5RnftB8WNX80T0NS7vSQsDOlg23diR2g==} + es-to-primitive@1.3.4: + resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} engines: {node: '>= 0.4'} esast-util-from-estree@2.0.0: @@ -2226,8 +2237,8 @@ packages: eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - expressive-code@0.43.1: - resolution: {integrity: sha512-JdOzanoU825iNvslmk6Kg8Ro61eSHmDK2Zz7BynOxObVrpIXZNzrIZOwQO2uDQcGsjSYShL/8vTrXgeWYnq3NA==} + expressive-code@0.44.0: + resolution: {integrity: sha512-JXVWVNCKlLuZLMQH8cOiDUSosT0Bb+elwE/dbAkpwFwDFmyFyWlECoWZIohh2FkIF1iI67TQJ+Ts9k7oNDh2qA==} extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -2446,8 +2457,8 @@ packages: http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} - i18next@26.3.1: - resolution: {integrity: sha512-txQqd5EULsqEh9OJqRH15aCaOuy/nLJyhw5EHCSKLKJE1aBbb3Zve2+uQIxgWhPm1QqUQoWyQBm2kfmmIrzkcQ==} + i18next@26.3.3: + resolution: {integrity: sha512-aYVegyBdXSO93CMMihvr47jI7GHSOcIahMpJX+qzUXDzW4xDJf2uenIA+45vDU+YhiVdcfsql70AC9RVdMNrHg==} peerDependencies: typescript: ^5 || ^6 peerDependenciesMeta: @@ -2457,8 +2468,8 @@ packages: idb@7.1.1: resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} - immutable@5.1.7: - resolution: {integrity: sha512-47Xb+LFbZ/ZIjQMj6Q5J3IfK7PJFuqRdFOC9FpGgRTK6U2dAEVmkR9hp58qU4FpYux5YXpneDwkj2EP6lppzFA==} + immutable@5.1.8: + resolution: {integrity: sha512-TM5YqrGeTsVIPPpILzeqZ8D2Zc2TvNgSDi88zPF2a4cyqQdWV/wVWBDRDbNzzrLeRWScrFcOX9lW2iX6GOtUDw==} inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} @@ -3007,8 +3018,8 @@ packages: node-mock-http@1.0.4: resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} - node-releases@2.0.48: - resolution: {integrity: sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==} + node-releases@2.0.50: + resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} engines: {node: '>=18'} normalize-path@3.0.0: @@ -3123,8 +3134,8 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} - prettier@3.8.4: - resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==} + prettier@3.8.5: + resolution: {integrity: sha512-zxcTTCedNGJM4R8sj/Cq/F0W/c4iE0afWBcBwMTRtw4WHYP9TWkYjdiH3npPRUYsXQCPR0hTU9yjovOu+E6EQA==} engines: {node: '>=14'} hasBin: true @@ -3227,8 +3238,8 @@ packages: resolution: {integrity: sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==} hasBin: true - rehype-expressive-code@0.43.1: - resolution: {integrity: sha512-CUOGQVlUcSMSXZgpcq9xL6B+dZqnI3w1R6EZj932XpGgj2Hmy7H6oMqa9W/Z7X2HOILWLWhqu1b9kuYcD+nd6w==} + rehype-expressive-code@0.44.0: + resolution: {integrity: sha512-5r74C5F2sMR3X+QJH8OKWgZBO/cqRw5W1fLT6GVlSfLqepk+4j8tGFkyPqZYWjwntsBHzKPDH2zI68sZ7ScLfA==} rehype-format@5.0.1: resolution: {integrity: sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ==} @@ -3304,8 +3315,8 @@ packages: retext@9.0.0: resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} - rolldown@1.0.3: - resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + rolldown@1.1.3: + resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -3334,8 +3345,8 @@ packages: engines: {node: '>=20.19.0'} hasBin: true - satteri@0.9.1: - resolution: {integrity: sha512-0oIBjwDxWvz8ePRSBxv8vEdZ0GI25/UcSq11y4651tJztUAmZlIdPjFx8luqBAM22g8YKVr5R17KaaZ2kIfLrw==} + satteri@0.9.3: + resolution: {integrity: sha512-2XfBh89LCnBMFkNOeVKkBLelAZcIA17VLHsgJum1tJ2fXiPZDN/TDXv4ku46rFOQXYd41LJ0kiZh5gPqExcCsg==} sax@1.6.0: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} @@ -3376,8 +3387,8 @@ packages: resolution: {integrity: sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==} engines: {node: '>=20.9.0'} - shiki@4.2.0: - resolution: {integrity: sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ==} + shiki@4.3.0: + resolution: {integrity: sha512-NKKjWzR6LIGL3sXBrWDw9sDS9cxx42/DkysaNqJEeOWE8Kix5gpak0bc00OfDVEO4oyXSyz8+aRaqKoBD1yo7A==} engines: {node: '>=20'} side-channel-list@1.0.1: @@ -3715,6 +3726,10 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + url-extras@0.1.0: + resolution: {integrity: sha512-8tzwTeXFPuX/5PHuCDQE5Dd9Ts4rwoq2t9aIT+HS4iAVpmj5l4Ao7Q+BuuFjvWRqrLswBhQDk8O96ZicgCqQqw==} + engines: {node: '>=20'} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -3739,13 +3754,13 @@ packages: '@vite-pwa/assets-generator': optional: true - vite@8.0.16: - resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + vite@8.1.0: + resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 + '@vitejs/devtools': ^0.3.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -4037,9 +4052,9 @@ snapshots: jsonpointer: 5.0.1 leven: 3.1.0 - '@astrojs/check@0.9.9(prettier@3.8.4)(typescript@6.0.3)': + '@astrojs/check@0.9.9(prettier@3.8.5)(typescript@6.0.3)': dependencies: - '@astrojs/language-server': 2.16.10(prettier@3.8.4)(typescript@6.0.3) + '@astrojs/language-server': 2.16.10(prettier@3.8.5)(typescript@6.0.3) chokidar: 4.0.3 kleur: 4.1.5 typescript: 6.0.3 @@ -4048,56 +4063,56 @@ snapshots: - prettier - prettier-plugin-astro - '@astrojs/compiler-binding-darwin-arm64@0.2.2': + '@astrojs/compiler-binding-darwin-arm64@0.2.3': optional: true - '@astrojs/compiler-binding-darwin-x64@0.2.2': + '@astrojs/compiler-binding-darwin-x64@0.2.3': optional: true - '@astrojs/compiler-binding-linux-arm64-gnu@0.2.2': + '@astrojs/compiler-binding-linux-arm64-gnu@0.2.3': optional: true - '@astrojs/compiler-binding-linux-arm64-musl@0.2.2': + '@astrojs/compiler-binding-linux-arm64-musl@0.2.3': optional: true - '@astrojs/compiler-binding-linux-x64-gnu@0.2.2': + '@astrojs/compiler-binding-linux-x64-gnu@0.2.3': optional: true - '@astrojs/compiler-binding-linux-x64-musl@0.2.2': + '@astrojs/compiler-binding-linux-x64-musl@0.2.3': optional: true - '@astrojs/compiler-binding-wasm32-wasi@0.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)': + '@astrojs/compiler-binding-wasm32-wasi@0.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: - '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1) + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' optional: true - '@astrojs/compiler-binding-win32-arm64-msvc@0.2.2': + '@astrojs/compiler-binding-win32-arm64-msvc@0.2.3': optional: true - '@astrojs/compiler-binding-win32-x64-msvc@0.2.2': + '@astrojs/compiler-binding-win32-x64-msvc@0.2.3': optional: true - '@astrojs/compiler-binding@0.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)': + '@astrojs/compiler-binding@0.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': optionalDependencies: - '@astrojs/compiler-binding-darwin-arm64': 0.2.2 - '@astrojs/compiler-binding-darwin-x64': 0.2.2 - '@astrojs/compiler-binding-linux-arm64-gnu': 0.2.2 - '@astrojs/compiler-binding-linux-arm64-musl': 0.2.2 - '@astrojs/compiler-binding-linux-x64-gnu': 0.2.2 - '@astrojs/compiler-binding-linux-x64-musl': 0.2.2 - '@astrojs/compiler-binding-wasm32-wasi': 0.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1) - '@astrojs/compiler-binding-win32-arm64-msvc': 0.2.2 - '@astrojs/compiler-binding-win32-x64-msvc': 0.2.2 + '@astrojs/compiler-binding-darwin-arm64': 0.2.3 + '@astrojs/compiler-binding-darwin-x64': 0.2.3 + '@astrojs/compiler-binding-linux-arm64-gnu': 0.2.3 + '@astrojs/compiler-binding-linux-arm64-musl': 0.2.3 + '@astrojs/compiler-binding-linux-x64-gnu': 0.2.3 + '@astrojs/compiler-binding-linux-x64-musl': 0.2.3 + '@astrojs/compiler-binding-wasm32-wasi': 0.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@astrojs/compiler-binding-win32-arm64-msvc': 0.2.3 + '@astrojs/compiler-binding-win32-x64-msvc': 0.2.3 transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' - '@astrojs/compiler-rs@0.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)': + '@astrojs/compiler-rs@0.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: - '@astrojs/compiler-binding': 0.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1) + '@astrojs/compiler-binding': 0.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -4111,11 +4126,11 @@ snapshots: js-yaml: 4.2.0 picomatch: 4.0.4 retext-smartypants: 6.2.0 - shiki: 4.2.0 + shiki: 4.3.0 smol-toml: 1.7.0 unified: 11.0.5 - '@astrojs/language-server@2.16.10(prettier@3.8.4)(typescript@6.0.3)': + '@astrojs/language-server@2.16.10(prettier@3.8.5)(typescript@6.0.3)': dependencies: '@astrojs/compiler': 2.13.1 '@astrojs/yaml2ts': 0.2.4 @@ -4129,14 +4144,14 @@ snapshots: volar-service-css: 0.0.70(@volar/language-service@2.4.28) volar-service-emmet: 0.0.70(@volar/language-service@2.4.28) volar-service-html: 0.0.70(@volar/language-service@2.4.28) - volar-service-prettier: 0.0.70(@volar/language-service@2.4.28)(prettier@3.8.4) + volar-service-prettier: 0.0.70(@volar/language-service@2.4.28)(prettier@3.8.5) volar-service-typescript: 0.0.70(@volar/language-service@2.4.28) volar-service-typescript-twoslash-queries: 0.0.70(@volar/language-service@2.4.28) volar-service-yaml: 0.0.70(@volar/language-service@2.4.28) vscode-html-languageservice: 5.6.2 vscode-uri: 3.1.0 optionalDependencies: - prettier: 3.8.4 + prettier: 3.8.5 transitivePeerDependencies: - typescript @@ -4167,15 +4182,15 @@ snapshots: '@astrojs/internal-helpers': 0.10.0 '@astrojs/prism': 4.0.2 github-slugger: 2.0.0 - satteri: 0.9.1 + satteri: 0.9.3 - '@astrojs/mdx@7.0.0(@astrojs/markdown-satteri@0.3.2)(astro@7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': + '@astrojs/mdx@7.0.0(@astrojs/markdown-satteri@0.3.2)(astro@7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: '@astrojs/internal-helpers': 0.10.0 '@astrojs/markdown-remark': 7.2.0 '@mdx-js/mdx': 3.1.1 acorn: 8.17.0 - astro: 7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + astro: 7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) es-module-lexer: 2.1.0 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 @@ -4200,12 +4215,12 @@ snapshots: '@astrojs/internal-helpers': 0.10.0 '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@vitejs/plugin-react': 5.2.0(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + '@vitejs/plugin-react': 5.2.0(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) devalue: 5.8.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) ultrahtml: 1.6.0 - vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - '@vitejs/devtools' @@ -4227,23 +4242,23 @@ snapshots: stream-replace-string: 2.0.0 zod: 4.4.3 - '@astrojs/starlight@0.41.0(@astrojs/markdown-remark@7.2.0)(astro@7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3)': + '@astrojs/starlight@0.41.1(@astrojs/markdown-remark@7.2.0)(astro@7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3)': dependencies: '@astrojs/markdown-satteri': 0.3.2 - '@astrojs/mdx': 7.0.0(@astrojs/markdown-satteri@0.3.2)(astro@7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + '@astrojs/mdx': 7.0.0(@astrojs/markdown-satteri@0.3.2)(astro@7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) '@astrojs/sitemap': 3.7.3 '@pagefind/default-ui': 1.5.2 '@types/hast': 3.0.4 '@types/js-yaml': 4.0.9 '@types/mdast': 4.0.4 - astro: 7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - astro-expressive-code: 0.43.1(astro@7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + astro: 7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + astro-expressive-code: 0.44.0(astro@7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) bcp-47: 2.1.0 hast-util-from-html: 2.0.3 hast-util-select: 6.0.4 hast-util-to-string: 3.0.1 hastscript: 9.0.1 - i18next: 26.3.1(typescript@6.0.3) + i18next: 26.3.3(typescript@6.0.3) js-yaml: 4.2.0 klona: 2.0.6 magic-string: 0.30.21 @@ -4254,7 +4269,7 @@ snapshots: rehype: 13.0.2 rehype-format: 5.0.1 remark-directive: 4.0.0 - satteri: 0.9.1 + satteri: 0.9.3 ultrahtml: 1.6.0 unified: 11.0.5 unist-util-visit: 5.1.0 @@ -4950,23 +4965,35 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@bruits/satteri-darwin-arm64@0.9.1': + '@bruits/satteri-darwin-arm64@0.9.3': + optional: true + + '@bruits/satteri-darwin-x64@0.9.3': + optional: true + + '@bruits/satteri-linux-arm64-gnu@0.9.3': optional: true - '@bruits/satteri-darwin-x64@0.9.1': + '@bruits/satteri-linux-arm64-musl@0.9.3': optional: true - '@bruits/satteri-linux-x64-gnu@0.9.1': + '@bruits/satteri-linux-x64-gnu@0.9.3': optional: true - '@bruits/satteri-wasm32-wasi@0.9.1': + '@bruits/satteri-linux-x64-musl@0.9.3': + optional: true + + '@bruits/satteri-wasm32-wasi@0.9.3': dependencies: - '@emnapi/core': 1.9.1 - '@emnapi/runtime': 1.9.1 - '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@bruits/satteri-win32-x64-msvc@0.9.1': + '@bruits/satteri-win32-arm64-msvc@0.9.3': + optional: true + + '@bruits/satteri-win32-x64-msvc@0.9.3': optional: true '@capsizecss/unpack@4.0.1': @@ -5010,20 +5037,9 @@ snapshots: '@emmetio/stream-reader@2.2.0': {} - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true - - '@emnapi/core@1.9.1': - dependencies: - '@emnapi/wasi-threads': 1.2.0 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.10.0': + '@emnapi/core@1.11.1': dependencies: + '@emnapi/wasi-threads': 1.2.2 tslib: 2.8.1 optional: true @@ -5032,17 +5048,7 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/runtime@1.9.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.1': + '@emnapi/wasi-threads@1.2.2': dependencies: tslib: 2.8.1 optional: true @@ -5125,7 +5131,7 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@expressive-code/core@0.43.1': + '@expressive-code/core@0.44.0': dependencies: '@ctrl/tinycolor': 4.2.0 hast-util-select: 6.0.4 @@ -5137,18 +5143,18 @@ snapshots: unist-util-visit: 5.1.0 unist-util-visit-parents: 6.0.2 - '@expressive-code/plugin-frames@0.43.1': + '@expressive-code/plugin-frames@0.44.0': dependencies: - '@expressive-code/core': 0.43.1 + '@expressive-code/core': 0.44.0 - '@expressive-code/plugin-shiki@0.43.1': + '@expressive-code/plugin-shiki@0.44.0': dependencies: - '@expressive-code/core': 0.43.1 - shiki: 4.2.0 + '@expressive-code/core': 0.44.0 + shiki: 4.3.0 - '@expressive-code/plugin-text-markers@0.43.1': + '@expressive-code/plugin-text-markers@0.44.0': dependencies: - '@expressive-code/core': 0.43.1 + '@expressive-code/core': 0.44.0 '@img/colour@1.1.0': {} @@ -5406,30 +5412,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.3 - optional: true - - '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)': + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: - '@emnapi/core': 1.10.0 + '@emnapi/core': 1.11.1 '@emnapi/runtime': 1.11.1 '@tybys/wasm-util': 0.10.3 optional: true - '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': - dependencies: - '@emnapi/core': 1.9.1 - '@emnapi/runtime': 1.9.1 - '@tybys/wasm-util': 0.10.3 - optional: true - '@oslojs/encoding@1.1.0': {} - '@oxc-project/types@0.133.0': {} + '@oxc-project/types@0.137.0': {} '@pagefind/darwin-arm64@1.5.2': optional: true @@ -5520,53 +5512,53 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@rolldown/binding-android-arm64@1.0.3': + '@rolldown/binding-android-arm64@1.1.3': optional: true - '@rolldown/binding-darwin-arm64@1.0.3': + '@rolldown/binding-darwin-arm64@1.1.3': optional: true - '@rolldown/binding-darwin-x64@1.0.3': + '@rolldown/binding-darwin-x64@1.1.3': optional: true - '@rolldown/binding-freebsd-x64@1.0.3': + '@rolldown/binding-freebsd-x64@1.1.3': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.3': + '@rolldown/binding-linux-arm64-gnu@1.1.3': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.3': + '@rolldown/binding-linux-arm64-musl@1.1.3': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.3': + '@rolldown/binding-linux-ppc64-gnu@1.1.3': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.3': + '@rolldown/binding-linux-s390x-gnu@1.1.3': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.3': + '@rolldown/binding-linux-x64-gnu@1.1.3': optional: true - '@rolldown/binding-linux-x64-musl@1.0.3': + '@rolldown/binding-linux-x64-musl@1.1.3': optional: true - '@rolldown/binding-openharmony-arm64@1.0.3': + '@rolldown/binding-openharmony-arm64@1.1.3': optional: true - '@rolldown/binding-wasm32-wasi@1.0.3': + '@rolldown/binding-wasm32-wasi@1.1.3': dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.3': + '@rolldown/binding-win32-arm64-msvc@1.1.3': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.3': + '@rolldown/binding-win32-x64-msvc@1.1.3': optional: true '@rolldown/pluginutils@1.0.0-rc.3': {} @@ -5623,40 +5615,40 @@ snapshots: optionalDependencies: rollup: 2.80.0 - '@shikijs/core@4.2.0': + '@shikijs/core@4.3.0': dependencies: - '@shikijs/primitive': 4.2.0 - '@shikijs/types': 4.2.0 + '@shikijs/primitive': 4.3.0 + '@shikijs/types': 4.3.0 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@4.2.0': + '@shikijs/engine-javascript@4.3.0': dependencies: - '@shikijs/types': 4.2.0 + '@shikijs/types': 4.3.0 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.6 - '@shikijs/engine-oniguruma@4.2.0': + '@shikijs/engine-oniguruma@4.3.0': dependencies: - '@shikijs/types': 4.2.0 + '@shikijs/types': 4.3.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@4.2.0': + '@shikijs/langs@4.3.0': dependencies: - '@shikijs/types': 4.2.0 + '@shikijs/types': 4.3.0 - '@shikijs/primitive@4.2.0': + '@shikijs/primitive@4.3.0': dependencies: - '@shikijs/types': 4.2.0 + '@shikijs/types': 4.3.0 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 - '@shikijs/themes@4.2.0': + '@shikijs/themes@4.3.0': dependencies: - '@shikijs/types': 4.2.0 + '@shikijs/types': 4.3.0 - '@shikijs/types@4.2.0': + '@shikijs/types@4.3.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -5752,12 +5744,12 @@ snapshots: '@ungap/structured-clone@1.3.2': {} - '@vite-pwa/astro@1.2.0(astro@7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1))': + '@vite-pwa/astro@1.2.0(astro@7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1))': dependencies: - astro: 7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - vite-plugin-pwa: 1.3.0(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1) + astro: 7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite-plugin-pwa: 1.3.0(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1) - '@vitejs/plugin-react@5.2.0(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': + '@vitejs/plugin-react@5.2.0(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) @@ -5765,7 +5757,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -5876,14 +5868,15 @@ snapshots: astring@1.9.0: {} - astro-expressive-code@0.43.1(astro@7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): + astro-expressive-code@0.44.0(astro@7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): dependencies: - astro: 7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - rehype-expressive-code: 0.43.1 + astro: 7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + rehype-expressive-code: 0.44.0 + url-extras: 0.1.0 - astro@7.0.2(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): + astro@7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): dependencies: - '@astrojs/compiler-rs': 0.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.1) + '@astrojs/compiler-rs': 0.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) '@astrojs/internal-helpers': 0.10.0 '@astrojs/markdown-satteri': 0.3.2 '@astrojs/telemetry': 3.3.2 @@ -5923,7 +5916,7 @@ snapshots: picomatch: 4.0.4 rehype: 13.0.2 semver: 7.8.5 - shiki: 4.2.0 + shiki: 4.3.0 smol-toml: 1.7.0 svgo: 4.0.1 tinyclip: 0.1.15 @@ -5934,8 +5927,8 @@ snapshots: unist-util-visit: 5.1.0 unstorage: 1.17.5 vfile: 6.0.3 - vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - vitefu: 1.1.3(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + vite: 8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vitefu: 1.1.3(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) xxhash-wasm: 1.1.0 yargs-parser: 22.0.0 zod: 4.4.3 @@ -6017,7 +6010,7 @@ snapshots: balanced-match@1.0.2: {} - baseline-browser-mapping@2.10.38: {} + baseline-browser-mapping@2.10.40: {} bcp-47-match@2.0.3: {} @@ -6040,10 +6033,10 @@ snapshots: browserslist@4.28.4: dependencies: - baseline-browser-mapping: 2.10.38 + baseline-browser-mapping: 2.10.40 caniuse-lite: 1.0.30001799 - electron-to-chromium: 1.5.377 - node-releases: 2.0.48 + electron-to-chromium: 1.5.379 + node-releases: 2.0.50 update-browserslist-db: 1.2.3(browserslist@4.28.4) buffer-from@1.1.2: {} @@ -6251,7 +6244,7 @@ snapshots: dependencies: jake: 10.9.4 - electron-to-chromium@1.5.377: {} + electron-to-chromium@1.5.379: {} emmet@2.4.11: dependencies: @@ -6285,7 +6278,7 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.2 es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.1 + es-to-primitive: 1.3.4 function.prototype.name: 1.2.0 get-intrinsic: 1.3.0 get-proto: 1.0.1 @@ -6345,9 +6338,10 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 - es-to-primitive@1.3.1: + es-to-primitive@1.3.4: dependencies: es-abstract-get: 1.0.0 + es-define-property: 1.0.1 es-errors: 1.3.0 is-callable: 1.2.7 is-date-object: 1.1.0 @@ -6441,12 +6435,12 @@ snapshots: eventemitter3@5.0.4: {} - expressive-code@0.43.1: + expressive-code@0.44.0: dependencies: - '@expressive-code/core': 0.43.1 - '@expressive-code/plugin-frames': 0.43.1 - '@expressive-code/plugin-shiki': 0.43.1 - '@expressive-code/plugin-text-markers': 0.43.1 + '@expressive-code/core': 0.44.0 + '@expressive-code/plugin-frames': 0.44.0 + '@expressive-code/plugin-shiki': 0.44.0 + '@expressive-code/plugin-text-markers': 0.44.0 extend@3.0.2: {} @@ -6801,13 +6795,13 @@ snapshots: http-cache-semantics@4.2.0: {} - i18next@26.3.1(typescript@6.0.3): + i18next@26.3.3(typescript@6.0.3): optionalDependencies: typescript: 6.0.3 idb@7.1.1: {} - immutable@5.1.7: {} + immutable@5.1.8: {} inflight@1.0.6: dependencies: @@ -7588,7 +7582,7 @@ snapshots: node-mock-http@1.0.4: {} - node-releases@2.0.48: {} + node-releases@2.0.50: {} normalize-path@3.0.0: {} @@ -7715,7 +7709,7 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prettier@3.8.4: {} + prettier@3.8.5: {} pretty-bytes@5.6.0: {} @@ -7828,9 +7822,9 @@ snapshots: dependencies: jsesc: 3.1.0 - rehype-expressive-code@0.43.1: + rehype-expressive-code@0.44.0: dependencies: - expressive-code: 0.43.1 + expressive-code: 0.44.0 rehype-format@5.0.1: dependencies: @@ -7969,26 +7963,26 @@ snapshots: retext-stringify: 4.0.0 unified: 11.0.5 - rolldown@1.0.3: + rolldown@1.1.3: dependencies: - '@oxc-project/types': 0.133.0 + '@oxc-project/types': 0.137.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.3 - '@rolldown/binding-darwin-arm64': 1.0.3 - '@rolldown/binding-darwin-x64': 1.0.3 - '@rolldown/binding-freebsd-x64': 1.0.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 - '@rolldown/binding-linux-arm64-gnu': 1.0.3 - '@rolldown/binding-linux-arm64-musl': 1.0.3 - '@rolldown/binding-linux-ppc64-gnu': 1.0.3 - '@rolldown/binding-linux-s390x-gnu': 1.0.3 - '@rolldown/binding-linux-x64-gnu': 1.0.3 - '@rolldown/binding-linux-x64-musl': 1.0.3 - '@rolldown/binding-openharmony-arm64': 1.0.3 - '@rolldown/binding-wasm32-wasi': 1.0.3 - '@rolldown/binding-win32-arm64-msvc': 1.0.3 - '@rolldown/binding-win32-x64-msvc': 1.0.3 + '@rolldown/binding-android-arm64': 1.1.3 + '@rolldown/binding-darwin-arm64': 1.1.3 + '@rolldown/binding-darwin-x64': 1.1.3 + '@rolldown/binding-freebsd-x64': 1.1.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.3 + '@rolldown/binding-linux-arm64-gnu': 1.1.3 + '@rolldown/binding-linux-arm64-musl': 1.1.3 + '@rolldown/binding-linux-ppc64-gnu': 1.1.3 + '@rolldown/binding-linux-s390x-gnu': 1.1.3 + '@rolldown/binding-linux-x64-gnu': 1.1.3 + '@rolldown/binding-linux-x64-musl': 1.1.3 + '@rolldown/binding-openharmony-arm64': 1.1.3 + '@rolldown/binding-wasm32-wasi': 1.1.3 + '@rolldown/binding-win32-arm64-msvc': 1.1.3 + '@rolldown/binding-win32-x64-msvc': 1.1.3 rollup@2.80.0: optionalDependencies: @@ -8018,23 +8012,27 @@ snapshots: sass@1.101.0: dependencies: chokidar: 5.0.0 - immutable: 5.1.7 + immutable: 5.1.8 source-map-js: 1.2.1 optionalDependencies: '@parcel/watcher': 2.5.6 - satteri@0.9.1: + satteri@0.9.3: dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 '@types/mdast': 4.0.4 '@types/unist': 3.0.3 optionalDependencies: - '@bruits/satteri-darwin-arm64': 0.9.1 - '@bruits/satteri-darwin-x64': 0.9.1 - '@bruits/satteri-linux-x64-gnu': 0.9.1 - '@bruits/satteri-wasm32-wasi': 0.9.1 - '@bruits/satteri-win32-x64-msvc': 0.9.1 + '@bruits/satteri-darwin-arm64': 0.9.3 + '@bruits/satteri-darwin-x64': 0.9.3 + '@bruits/satteri-linux-arm64-gnu': 0.9.3 + '@bruits/satteri-linux-arm64-musl': 0.9.3 + '@bruits/satteri-linux-x64-gnu': 0.9.3 + '@bruits/satteri-linux-x64-musl': 0.9.3 + '@bruits/satteri-wasm32-wasi': 0.9.3 + '@bruits/satteri-win32-arm64-msvc': 0.9.3 + '@bruits/satteri-win32-x64-msvc': 0.9.3 sax@1.6.0: {} @@ -8134,14 +8132,14 @@ snapshots: '@img/sharp-win32-ia32': 0.35.2 '@img/sharp-win32-x64': 0.35.2 - shiki@4.2.0: + shiki@4.3.0: dependencies: - '@shikijs/core': 4.2.0 - '@shikijs/engine-javascript': 4.2.0 - '@shikijs/engine-oniguruma': 4.2.0 - '@shikijs/langs': 4.2.0 - '@shikijs/themes': 4.2.0 - '@shikijs/types': 4.2.0 + '@shikijs/core': 4.3.0 + '@shikijs/engine-javascript': 4.3.0 + '@shikijs/engine-oniguruma': 4.3.0 + '@shikijs/langs': 4.3.0 + '@shikijs/themes': 4.3.0 + '@shikijs/types': 4.3.0 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -8489,6 +8487,8 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + url-extras@0.1.0: {} + util-deprecate@1.0.2: {} vfile-location@5.0.3: @@ -8506,23 +8506,23 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-pwa@1.3.0(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1): + vite-plugin-pwa@1.3.0(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1): dependencies: debug: 4.4.3 pretty-bytes: 6.1.1 tinyglobby: 0.2.17 - vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) workbox-build: 7.3.0(@types/babel__core@7.20.5) workbox-window: 7.4.1 transitivePeerDependencies: - supports-color - vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): + vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 postcss: 8.5.15 - rolldown: 1.0.3 + rolldown: 1.1.3 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.13.2 @@ -8532,9 +8532,9 @@ snapshots: terser: 5.48.0 yaml: 2.9.0 - vitefu@1.1.3(vite@8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): + vitefu@1.1.3(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): optionalDependencies: - vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) volar-service-css@0.0.70(@volar/language-service@2.4.28): dependencies: @@ -8561,12 +8561,12 @@ snapshots: optionalDependencies: '@volar/language-service': 2.4.28 - volar-service-prettier@0.0.70(@volar/language-service@2.4.28)(prettier@3.8.4): + volar-service-prettier@0.0.70(@volar/language-service@2.4.28)(prettier@3.8.5): dependencies: vscode-uri: 3.1.0 optionalDependencies: '@volar/language-service': 2.4.28 - prettier: 3.8.4 + prettier: 3.8.5 volar-service-typescript-twoslash-queries@0.0.70(@volar/language-service@2.4.28): dependencies: @@ -8834,7 +8834,7 @@ snapshots: '@vscode/l10n': 0.0.18 ajv: 8.20.0 ajv-draft-04: 1.0.0(ajv@8.20.0) - prettier: 3.8.4 + prettier: 3.8.5 request-light: 0.5.8 vscode-json-languageservice: 4.1.8 vscode-languageserver: 9.0.1 diff --git a/docs/src/content/docs/docs/v2/onenote.md b/docs/src/content/docs/docs/v2/onenote.md new file mode 100644 index 000000000000..52edcd0f0285 --- /dev/null +++ b/docs/src/content/docs/docs/v2/onenote.md @@ -0,0 +1,290 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. +2. Select the section that you want to export. +3. Select **File**. +4. Select **Export**. +5. Under **Export Current**, select **Section**. +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. +8. Choose a local folder and enter a name for the section. +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Acknowledgements + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. From c3399ab8f87aee0c3178e76576790c30ab613826 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Fri, 26 Jun 2026 21:55:48 +0200 Subject: [PATCH 016/117] Start adding pages selector --- app/lib/dialogs/import/pages.dart | 25 ++++++ app/lib/dialogs/pages.dart | 97 ++++++++++++++++++++++-- app/lib/dialogs/template.dart | 38 ++++------ app/lib/views/navigator/pages.dart | 42 ++++++++++ app/test/views/navigator/pages_test.dart | 17 +++++ 5 files changed, 189 insertions(+), 30 deletions(-) diff --git a/app/lib/dialogs/import/pages.dart b/app/lib/dialogs/import/pages.dart index daae786af9e5..0b6a4ca8e893 100644 --- a/app/lib/dialogs/import/pages.dart +++ b/app/lib/dialogs/import/pages.dart @@ -9,6 +9,8 @@ import 'package:butterfly/src/generated/i18n/app_localizations.dart'; import 'package:material_leap/material_leap.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; +import '../pages.dart'; + @immutable class PageDialogCallback { final List pages; @@ -81,6 +83,29 @@ class _ImportPagesDialogState extends State { }); }, ), + IconButton( + tooltip: AppLocalizations.of(context).selectPages, + icon: const PhosphorIcon(PhosphorIconsLight.listNumbers), + onPressed: () async { + final selected = await showDialog>( + context: context, + builder: (context) => SelectPagesDialog( + pages: List.generate( + widget.pages.length, + (index) => ( + AppLocalizations.of(context).pageIndex(index + 1), + '$index', + ), + ), + initialSelected: _selected.map((e) => '$e'), + ), + ); + if (selected == null) return; + setState(() { + _selected = selected.map(int.parse).toList()..sort(); + }); + }, + ), ], ), Flexible( diff --git a/app/lib/dialogs/pages.dart b/app/lib/dialogs/pages.dart index de85fe200bc9..5c5af6e9513c 100644 --- a/app/lib/dialogs/pages.dart +++ b/app/lib/dialogs/pages.dart @@ -3,8 +3,13 @@ import 'package:flutter/material.dart'; class SelectPagesDialog extends StatefulWidget { final List<(String name, String id)> pages; + final Iterable initialSelected; - const SelectPagesDialog({super.key, required this.pages}); + const SelectPagesDialog({ + super.key, + required this.pages, + this.initialSelected = const [], + }); @override State createState() => _SelectPagesDialogState(); @@ -12,6 +17,36 @@ class SelectPagesDialog extends StatefulWidget { class _SelectPagesDialogState extends State { final List _selected = []; + late final TextEditingController _rangeController; + String? _rangeError; + + @override + void initState() { + super.initState(); + final pageIds = widget.pages.map((e) => e.$2).toSet(); + _selected.addAll(widget.initialSelected.where(pageIds.contains)); + _rangeController = TextEditingController(); + } + + @override + void dispose() { + _rangeController.dispose(); + super.dispose(); + } + + void _applyRange(String value) { + final selectedIndexes = parsePageSelection(value, widget.pages.length); + setState(() { + if (selectedIndexes == null) { + _rangeError = AppLocalizations.of(context).error; + return; + } + _rangeError = null; + _selected + ..clear() + ..addAll(selectedIndexes.map((index) => widget.pages[index].$2)); + }); + } @override Widget build(BuildContext context) { @@ -35,18 +70,34 @@ class _SelectPagesDialogState extends State { }); }, ), - ...widget.pages.map( - (page) => CheckboxListTile( + TextField( + controller: _rangeController, + decoration: InputDecoration( + labelText: AppLocalizations.of(context).pages, + hintText: '1-3, 5', + errorText: _rangeError, + filled: true, + ), + onChanged: _applyRange, + onSubmitted: _applyRange, + ), + ...widget.pages.asMap().entries.map( + (entry) => CheckboxListTile( title: Text( - page.$1.isEmpty ? AppLocalizations.of(context).page : page.$1, + entry.value.$1.isEmpty + ? AppLocalizations.of(context).page + : entry.value.$1, + ), + subtitle: Text( + AppLocalizations.of(context).pageIndex(entry.key + 1), ), - value: _selected.contains(page.$2), + value: _selected.contains(entry.value.$2), onChanged: (v) { setState(() { if (v == true) { - _selected.add(page.$2); + _selected.add(entry.value.$2); } else { - _selected.remove(page.$2); + _selected.remove(entry.value.$2); } }); }, @@ -69,3 +120,35 @@ class _SelectPagesDialogState extends State { ); } } + +List? parsePageSelection(String value, int pageCount) { + final normalized = value + .trim() + .replaceAll('[', '') + .replaceAll(']', '') + .replaceAll('\u2013', '-') + .replaceAll('\u2014', '-'); + if (normalized.isEmpty) return const []; + final selected = {}; + for (final part in normalized.split(RegExp(r'[,;]'))) { + final token = part.trim(); + if (token.isEmpty) return null; + final bounds = token.split('-').map((e) => e.trim()).toList(); + if (bounds.length > 2 || bounds.any((e) => e.isEmpty)) return null; + final start = int.tryParse(bounds.first); + final end = int.tryParse(bounds.last); + if (start == null || end == null) return null; + if (start < 1 || end < 1 || start > pageCount || end > pageCount) { + return null; + } + final lower = start < end ? start : end; + final upper = start < end ? end : start; + for (var index = lower - 1; index < upper; index++) { + selected.add(index); + } + } + return List.generate( + pageCount, + (index) => index, + ).where(selected.contains).toList(); +} diff --git a/app/lib/dialogs/template.dart b/app/lib/dialogs/template.dart index afb10a235a2d..5139702c6129 100644 --- a/app/lib/dialogs/template.dart +++ b/app/lib/dialogs/template.dart @@ -22,6 +22,7 @@ import '../bloc/document_bloc.dart'; import '../widgets/editable_list_tile.dart'; import 'area/init.dart'; import 'delete.dart'; +import 'pages.dart'; Future _overrideTools( TemplateFileSystem templateSystem, @@ -1341,31 +1342,22 @@ List _buildTemplateMenuChildren( }, ), if (bloc != null && templateBackgrounds.isNotEmpty) - SubmenuButton( + MenuItemButton( leadingIcon: const PhosphorIcon(PhosphorIconsLight.image), - menuChildren: [ - MenuItemButton( - leadingIcon: const PhosphorIcon(PhosphorIconsLight.file), - child: Text(AppLocalizations.of(context).currentPage), - onPressed: () { - _applyTemplateBackgroundsToPages(bloc, template, [null]); - }, - ), - MenuItemButton( - leadingIcon: const PhosphorIcon(PhosphorIconsLight.files), - child: Text(AppLocalizations.of(context).allPages), - onPressed: () { - final state = bloc.state; - if (state is! DocumentLoadSuccess) return; - _applyTemplateBackgroundsToPages( - bloc, - template, - state.data.getPages(true), - ); - }, - ), - ], child: Text(AppLocalizations.of(context).applyBackground), + onPressed: () async { + final state = bloc.state; + if (state is! DocumentLoadSuccess) return; + final selectedPageNames = await showDialog>( + context: context, + builder: (context) => SelectPagesDialog( + pages: state.data.getPagesWithNames(), + initialSelected: [state.pageName], + ), + ); + if (selectedPageNames == null) return; + _applyTemplateBackgroundsToPages(bloc, template, selectedPageNames); + }, ), MenuItemButton( leadingIcon: const PhosphorIcon(PhosphorIconsLight.copy), diff --git a/app/lib/views/navigator/pages.dart b/app/lib/views/navigator/pages.dart index 1aed5db0ce1c..ea5c0a0b126b 100644 --- a/app/lib/views/navigator/pages.dart +++ b/app/lib/views/navigator/pages.dart @@ -9,6 +9,7 @@ import 'package:material_leap/l10n/leap_localizations.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../../dialogs/delete.dart'; +import '../../dialogs/pages.dart' as pages_dialog; import '../../widgets/editable_list_tile.dart'; typedef PageEntity = ({String path, String name, bool isFile}); @@ -78,10 +79,13 @@ class PagesView extends StatefulWidget { class _PagesViewState extends State { final TextEditingController _locationController = TextEditingController(); + final TextEditingController _rangeController = TextEditingController(); + String? _rangeError; @override void dispose() { _locationController.dispose(); + _rangeController.dispose(); super.dispose(); } @@ -156,6 +160,44 @@ class _PagesViewState extends State { child: OverflowBar( spacing: 8, children: [ + SizedBox( + width: 180, + child: TextField( + controller: _rangeController, + decoration: InputDecoration( + labelText: AppLocalizations.of(context).pages, + hintText: '1-3, 5', + errorText: _rangeError, + filled: true, + isDense: true, + ), + onChanged: (value) { + final selectablePages = all + .where((entity) => entity.isFile) + .toList(); + final selectedIndexes = pages_dialog + .parsePageSelection( + value, + selectablePages.length, + ); + setState(() { + if (selectedIndexes == null) { + _rangeError = AppLocalizations.of( + context, + ).error; + return; + } + _rangeError = null; + controller.clear(); + controller.selectAll( + selectedIndexes.map( + (index) => selectablePages[index].path, + ), + ); + }); + }, + ), + ), ActionChip( label: Text(AppLocalizations.of(context).delete), avatar: const PhosphorIcon( diff --git a/app/test/views/navigator/pages_test.dart b/app/test/views/navigator/pages_test.dart index 9e4bff214613..b28a462ff268 100644 --- a/app/test/views/navigator/pages_test.dart +++ b/app/test/views/navigator/pages_test.dart @@ -3,6 +3,7 @@ import 'package:butterfly/bloc/document_bloc.dart'; import 'package:butterfly/cubits/current_index.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/transform.dart'; +import 'package:butterfly/dialogs/pages.dart' as pages_dialog; import 'package:butterfly/models/viewport.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; import 'package:butterfly/views/navigator/pages.dart'; @@ -75,6 +76,22 @@ void main() { }); }); + group('page selection parser', () { + test('accepts individual pages and ranges', () { + expect(pages_dialog.parsePageSelection('[2-4, 6]', 8), [1, 2, 3, 5]); + }); + + test('keeps document order and accepts reversed ranges', () { + expect(pages_dialog.parsePageSelection('5-3, 2', 6), [1, 2, 3, 4]); + }); + + test('rejects out of bounds and malformed input', () { + expect(pages_dialog.parsePageSelection('0, 2', 4), isNull); + expect(pages_dialog.parsePageSelection('2-', 4), isNull); + expect(pages_dialog.parsePageSelection('5', 4), isNull); + }); + }); + testWidgets('dragging pages updates document and visible order', ( tester, ) async { From d657a37d8775ee0d26de91670dc759e575287e26 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 28 Jun 2026 10:10:26 +0200 Subject: [PATCH 017/117] Improve page range selection UI --- app/lib/dialogs/pages.dart | 217 ++++++++--- app/lib/views/navigator/pages.dart | 467 +++++++++++++++-------- app/lib/widgets/multi_select.dart | 396 +++++++++---------- app/test/views/navigator/pages_test.dart | 9 + 4 files changed, 690 insertions(+), 399 deletions(-) diff --git a/app/lib/dialogs/pages.dart b/app/lib/dialogs/pages.dart index 5c5af6e9513c..9768fd3b79ce 100644 --- a/app/lib/dialogs/pages.dart +++ b/app/lib/dialogs/pages.dart @@ -1,5 +1,8 @@ +import 'dart:math' as math; + import 'package:butterfly/src/generated/i18n/app_localizations.dart'; import 'package:flutter/material.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; class SelectPagesDialog extends StatefulWidget { final List<(String name, String id)> pages; @@ -16,7 +19,7 @@ class SelectPagesDialog extends StatefulWidget { } class _SelectPagesDialogState extends State { - final List _selected = []; + final Set _selected = {}; late final TextEditingController _rangeController; String? _rangeError; @@ -26,6 +29,7 @@ class _SelectPagesDialogState extends State { final pageIds = widget.pages.map((e) => e.$2).toSet(); _selected.addAll(widget.initialSelected.where(pageIds.contains)); _rangeController = TextEditingController(); + _syncRangeText(); } @override @@ -48,62 +52,139 @@ class _SelectPagesDialogState extends State { }); } + void _syncRangeText() { + final selectedIndexes = widget.pages + .asMap() + .entries + .where((entry) => _selected.contains(entry.value.$2)) + .map((entry) => entry.key); + final text = formatPageSelection(selectedIndexes); + if (_rangeController.text == text) return; + _rangeController.value = TextEditingValue( + text: text, + selection: TextSelection.collapsed(offset: text.length), + ); + } + + void _updateSelected(void Function() update) { + setState(() { + update(); + _rangeError = null; + _syncRangeText(); + }); + } + @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + final colorScheme = ColorScheme.of(context); + final selectionCount = _selected.length; + final everythingSelected = selectionCount == widget.pages.length; return AlertDialog( - title: Text(AppLocalizations.of(context).selectPages), - scrollable: true, - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CheckboxListTile( - title: Text(AppLocalizations.of(context).selectAll), - value: _selected.length == widget.pages.length, - onChanged: (v) { - setState(() { - if (v == true) { - _selected.clear(); - _selected.addAll(widget.pages.map((e) => e.$2)); - } else { - _selected.clear(); - } - }); - }, - ), - TextField( - controller: _rangeController, - decoration: InputDecoration( - labelText: AppLocalizations.of(context).pages, - hintText: '1-3, 5', - errorText: _rangeError, - filled: true, - ), - onChanged: _applyRange, - onSubmitted: _applyRange, - ), - ...widget.pages.asMap().entries.map( - (entry) => CheckboxListTile( - title: Text( - entry.value.$1.isEmpty - ? AppLocalizations.of(context).page - : entry.value.$1, + title: Text(loc.selectPages), + contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0), + content: SizedBox( + width: 520, + height: math.min(MediaQuery.sizeOf(context).height * 0.65, 560), + child: Column( + children: [ + DecoratedBox( + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: const BorderRadius.all(Radius.circular(8)), ), - subtitle: Text( - AppLocalizations.of(context).pageIndex(entry.key + 1), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + spacing: 12, + children: [ + Row( + children: [ + Expanded( + child: Text( + loc.countPages(selectionCount), + style: TextTheme.of(context).titleMedium, + ), + ), + IconButton( + icon: PhosphorIcon( + everythingSelected + ? PhosphorIconsLight.selectionSlash + : PhosphorIconsLight.selectionAll, + ), + tooltip: everythingSelected + ? loc.deselect + : loc.selectAll, + onPressed: () => _updateSelected(() { + _selected.clear(); + if (!everythingSelected) { + _selected.addAll(widget.pages.map((e) => e.$2)); + } + }), + ), + ], + ), + TextField( + controller: _rangeController, + decoration: InputDecoration( + prefixIcon: const PhosphorIcon( + PhosphorIconsLight.listNumbers, + ), + labelText: loc.pages, + hintText: '1-3, 5', + errorText: _rangeError, + filled: true, + isDense: true, + suffixIcon: _rangeController.text.isEmpty + ? null + : IconButton( + icon: const PhosphorIcon(PhosphorIconsLight.x), + tooltip: MaterialLocalizations.of( + context, + ).deleteButtonTooltip, + onPressed: () => + _updateSelected(_selected.clear), + ), + ), + onChanged: _applyRange, + onSubmitted: _applyRange, + ), + ], + ), ), - value: _selected.contains(entry.value.$2), - onChanged: (v) { - setState(() { - if (v == true) { - _selected.add(entry.value.$2); - } else { - _selected.remove(entry.value.$2); - } - }); - }, ), - ), - ], + const SizedBox(height: 12), + Expanded( + child: Material( + type: MaterialType.transparency, + child: ListView.separated( + itemCount: widget.pages.length, + separatorBuilder: (context, index) => + const Divider(height: 1), + itemBuilder: (context, index) { + final page = widget.pages[index]; + final id = page.$2; + return CheckboxListTile( + contentPadding: EdgeInsets.zero, + title: Text(page.$1.isEmpty ? loc.page : page.$1), + subtitle: Text(loc.pageIndex(index + 1)), + value: _selected.contains(id), + onChanged: (v) { + _updateSelected(() { + if (v == true) { + _selected.add(id); + } else { + _selected.remove(id); + } + }); + }, + ); + }, + ), + ), + ), + ], + ), ), actions: [ TextButton( @@ -113,14 +194,44 @@ class _SelectPagesDialogState extends State { ElevatedButton( onPressed: _selected.isEmpty ? null - : () => Navigator.pop(context, _selected), - child: Text(AppLocalizations.of(context).select), + : () => Navigator.pop( + context, + widget.pages + .where((page) => _selected.contains(page.$2)) + .map((page) => page.$2) + .toList(), + ), + child: Text(loc.select), ), ], ); } } +String formatPageSelection(Iterable indexes) { + final sorted = indexes.toSet().toList()..sort(); + if (sorted.isEmpty) return ''; + final ranges = []; + var start = sorted.first; + var previous = start; + for (final index in sorted.skip(1)) { + if (index == previous + 1) { + previous = index; + continue; + } + ranges.add(_formatPageRange(start, previous)); + start = previous = index; + } + ranges.add(_formatPageRange(start, previous)); + return ranges.join(', '); +} + +String _formatPageRange(int start, int end) { + final first = start + 1; + final last = end + 1; + return first == last ? '$first' : '$first-$last'; +} + List? parsePageSelection(String value, int pageCount) { final normalized = value .trim() diff --git a/app/lib/views/navigator/pages.dart b/app/lib/views/navigator/pages.dart index ea5c0a0b126b..a2e333e95dce 100644 --- a/app/lib/views/navigator/pages.dart +++ b/app/lib/views/navigator/pages.dart @@ -80,15 +80,70 @@ class PagesView extends StatefulWidget { class _PagesViewState extends State { final TextEditingController _locationController = TextEditingController(); final TextEditingController _rangeController = TextEditingController(); + final FocusNode _rangeFocusNode = FocusNode(); + final MultiSelectController _selectionController = + MultiSelectController(); String? _rangeError; + bool _updatingRangeText = false; @override void dispose() { _locationController.dispose(); _rangeController.dispose(); + _rangeFocusNode.dispose(); + _selectionController.dispose(); super.dispose(); } + void _setRangeText(String text) { + if (_rangeController.text == text) return; + _updatingRangeText = true; + _rangeController.value = TextEditingValue( + text: text, + selection: TextSelection.collapsed(offset: text.length), + ); + _updatingRangeText = false; + } + + void _syncRangeTextFromSelection( + MultiSelectController controller, + List selectablePages, + ) { + if (_rangeError != null || _rangeFocusNode.hasFocus) return; + final selectedIndexes = selectablePages + .asMap() + .entries + .where((entry) => controller.selectedIds.contains(entry.value.path)) + .map((entry) => entry.key); + _setRangeText(pages_dialog.formatPageSelection(selectedIndexes)); + } + + void _applyRangeSelection( + String value, + List selectablePages, { + bool normalizeText = false, + }) { + if (_updatingRangeText) return; + final selectedIndexes = pages_dialog.parsePageSelection( + value, + selectablePages.length, + ); + setState(() { + if (selectedIndexes == null) { + _rangeError = AppLocalizations.of(context).error; + return; + } + _rangeError = null; + _selectionController.clear(); + _selectionController.selectAll( + selectedIndexes.map((index) => selectablePages[index].path), + ); + if (normalizeText) { + _setRangeText(pages_dialog.formatPageSelection(selectedIndexes)); + } + }); + } + @override Widget build(BuildContext context) { return BlocBuilder( @@ -152,77 +207,7 @@ class _PagesViewState extends State { return Material( type: MaterialType.transparency, child: MultiSelectRegion( - toolbarBuilder: (context, controller) => Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 4.0, - ), - child: OverflowBar( - spacing: 8, - children: [ - SizedBox( - width: 180, - child: TextField( - controller: _rangeController, - decoration: InputDecoration( - labelText: AppLocalizations.of(context).pages, - hintText: '1-3, 5', - errorText: _rangeError, - filled: true, - isDense: true, - ), - onChanged: (value) { - final selectablePages = all - .where((entity) => entity.isFile) - .toList(); - final selectedIndexes = pages_dialog - .parsePageSelection( - value, - selectablePages.length, - ); - setState(() { - if (selectedIndexes == null) { - _rangeError = AppLocalizations.of( - context, - ).error; - return; - } - _rangeError = null; - controller.clear(); - controller.selectAll( - selectedIndexes.map( - (index) => selectablePages[index].path, - ), - ); - }); - }, - ), - ), - ActionChip( - label: Text(AppLocalizations.of(context).delete), - avatar: const PhosphorIcon( - PhosphorIconsLight.trash, - ), - onPressed: () async { - if (controller.selectedIds.isEmpty) return; - final result = await showDialog( - context: context, - builder: (context) => const DeleteDialog(), - ); - if (result != true) return; - if (!context.mounted) return; - final bloc = context.read(); - // Remember that for page deletion it accepts an array. - // If the event exists in the future, it might be used. - for (final id in controller.selectedIds) { - bloc.add(PageRemoved(id)); - } - controller.clear(); - }, - ), - ], - ), - ), + controller: _selectionController, builder: (context, controller, child) => ReorderableListView.builder( buildDefaultDragHandles: false, @@ -272,6 +257,9 @@ class _PagesViewState extends State { controller: controller, data: state.data, index: index, + onSelectionChanged: () { + setState(() => _rangeError = null); + }, key: ValueKey(entity.path), ); }, @@ -281,90 +269,252 @@ class _PagesViewState extends State { }, ), ), - Card.filled( - child: SizedBox( - height: 64, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 4, - vertical: 8, + ValueListenableBuilder( + valueListenable: _locationController, + builder: (context, value, child) { + final selectablePages = buildPageEntitiesForLocation( + pages, + value.text, + ).where((entity) => entity.isFile).toList(); + return ListenableBuilder( + listenable: _selectionController, + builder: (context, child) { + if (_selectionController.selectionMode) { + _syncRangeTextFromSelection( + _selectionController, + selectablePages, + ); + return _PagesSelectionBar( + controller: _selectionController, + rangeController: _rangeController, + rangeFocusNode: _rangeFocusNode, + rangeError: _rangeError, + selectablePages: selectablePages, + onRangeChanged: (value) => + _applyRangeSelection(value, selectablePages), + onRangeSubmitted: (value) => _applyRangeSelection( + value, + selectablePages, + normalizeText: true, + ), + onClear: () { + setState(() { + _rangeError = null; + _selectionController.clear(); + _setRangeText(''); + }); + }, + onSelectAllChanged: () { + final everythingSelected = + _selectionController.selectedIds.length == + selectablePages.length; + setState(() { + _rangeError = null; + _selectionController.clear(); + if (!everythingSelected) { + _selectionController.selectAll( + selectablePages.map((entity) => entity.path), + ); + } + }); + }, + onDelete: _selectionController.selectedIds.isEmpty + ? null + : () async { + final result = await showDialog( + context: context, + builder: (context) => const DeleteDialog(), + ); + if (result != true) return; + if (!context.mounted) return; + final bloc = context.read(); + for (final id + in _selectionController.selectedIds) { + bloc.add(PageRemoved(id)); + } + _selectionController.clear(); + _setRangeText(''); + }, + ); + } + return _PagesCreateBar(index: index, onAddPage: addPage); + }, + ); + }, + ), + ], + ); + }, + ); + } +} + +class _PagesSelectionBar extends StatelessWidget { + const _PagesSelectionBar({ + required this.controller, + required this.rangeController, + required this.rangeFocusNode, + required this.rangeError, + required this.selectablePages, + required this.onRangeChanged, + required this.onRangeSubmitted, + required this.onClear, + required this.onSelectAllChanged, + required this.onDelete, + }); + + final MultiSelectController controller; + final TextEditingController rangeController; + final FocusNode rangeFocusNode; + final String? rangeError; + final List selectablePages; + final ValueChanged onRangeChanged; + final ValueChanged onRangeSubmitted; + final VoidCallback onClear; + final VoidCallback onSelectAllChanged; + final VoidCallback? onDelete; + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + final everythingSelected = + controller.selectedIds.length == selectablePages.length; + return Card.filled( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + IconButton( + icon: const PhosphorIcon(PhosphorIconsLight.x), + tooltip: MaterialLocalizations.of(context).closeButtonTooltip, + onPressed: onClear, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + loc.countPages(controller.selectedIds.length), + style: TextTheme.of(context).titleSmall, + overflow: TextOverflow.ellipsis, + ), + ), + IconButton.filledTonal( + icon: PhosphorIcon( + everythingSelected + ? PhosphorIconsLight.selectionSlash + : PhosphorIconsLight.selectionAll, ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Row( - spacing: 8, - children: [ - const PhosphorIcon(PhosphorIconsLight.plus), - Text( - LeapLocalizations.of(context).create, - style: TextTheme.of(context).titleMedium, - ), - ], + tooltip: everythingSelected ? loc.deselect : loc.selectAll, + onPressed: onSelectAllChanged, + ), + const SizedBox(width: 8), + IconButton.filledTonal( + icon: const PhosphorIcon(PhosphorIconsLight.trash), + tooltip: loc.delete, + onPressed: onDelete, + ), + ], + ), + const SizedBox(height: 6), + TextField( + controller: rangeController, + focusNode: rangeFocusNode, + decoration: InputDecoration( + prefixIcon: const PhosphorIcon(PhosphorIconsLight.listNumbers), + labelText: loc.pages, + hintText: '1-3, 5', + errorText: rangeError, + filled: true, + isDense: true, + ), + onChanged: onRangeChanged, + onSubmitted: onRangeSubmitted, + ), + ], + ), + ), + ); + } +} + +class _PagesCreateBar extends StatelessWidget { + const _PagesCreateBar({required this.index, required this.onAddPage}); + + final int? index; + final void Function([int? index]) onAddPage; + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return Card.filled( + child: SizedBox( + height: 64, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + spacing: 8, + children: [ + const PhosphorIcon(PhosphorIconsLight.plus), + Text( + LeapLocalizations.of(context).create, + style: TextTheme.of(context).titleMedium, + ), + ], + ), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + IconButton.filledTonal( + icon: const PhosphorIcon(PhosphorIconsLight.arrowUp), + tooltip: loc.insertBefore, + onPressed: () => onAddPage(index), + ), + const SizedBox(width: 8), + IconButton.filledTonal( + icon: const PhosphorIcon( + PhosphorIconsLight.arrowDown, + ), + tooltip: loc.insertAfter, + onPressed: () => onAddPage((index ?? -1) + 1), ), - ), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - IconButton.filledTonal( - icon: const PhosphorIcon( - PhosphorIconsLight.arrowUp, - ), - tooltip: AppLocalizations.of( - context, - ).insertBefore, - onPressed: () => addPage(index), - ), - const SizedBox(width: 8), - IconButton.filledTonal( - icon: const PhosphorIcon( - PhosphorIconsLight.arrowDown, - ), - tooltip: AppLocalizations.of( - context, - ).insertAfter, - onPressed: () => addPage((index ?? -1) + 1), - ), - const SizedBox(width: 8), - IconButton.filledTonal( - icon: const PhosphorIcon( - PhosphorIconsLight.arrowLineUp, - ), - tooltip: AppLocalizations.of( - context, - ).insertFirst, - onPressed: () => addPage(0), - ), - const SizedBox(width: 8), - IconButton.filledTonal( - icon: const PhosphorIcon( - PhosphorIconsLight.arrowLineDown, - ), - tooltip: AppLocalizations.of( - context, - ).insertLast, - onPressed: () => addPage(), - ), - const SizedBox(width: 8), - ], - ), + const SizedBox(width: 8), + IconButton.filledTonal( + icon: const PhosphorIcon( + PhosphorIconsLight.arrowLineUp, ), + tooltip: loc.insertFirst, + onPressed: () => onAddPage(0), ), - ), - ], + const SizedBox(width: 8), + IconButton.filledTonal( + icon: const PhosphorIcon( + PhosphorIconsLight.arrowLineDown, + ), + tooltip: loc.insertLast, + onPressed: () => onAddPage(), + ), + const SizedBox(width: 8), + ], + ), ), ), ), - ), - ], - ); - }, + ], + ), + ), + ), ); } } @@ -377,6 +527,7 @@ class _PageEntityListTile extends StatelessWidget { required this.controller, required this.data, required this.index, + required this.onSelectionChanged, super.key, }); @@ -386,6 +537,7 @@ class _PageEntityListTile extends StatelessWidget { final int index; final TextEditingController locationController; final MultiSelectController controller; + final VoidCallback onSelectionChanged; @override Widget build(BuildContext context) { @@ -401,10 +553,14 @@ class _PageEntityListTile extends StatelessWidget { child: EditableListTile( initialValue: entity.name, selected: isSelected, + showEditIcon: !isSelectionMode, leading: isSelectionMode && editable ? Checkbox( value: isSelected, - onChanged: (value) => controller.toggle(entity.path), + onChanged: (value) { + controller.toggle(entity.path); + onSelectionChanged(); + }, ) : Icon( editable @@ -417,6 +573,7 @@ class _PageEntityListTile extends StatelessWidget { onTap: () { if (isSelectionMode && editable) { controller.toggle(entity.path); + onSelectionChanged(); return; } if (editable) { @@ -429,9 +586,10 @@ class _PageEntityListTile extends StatelessWidget { if (!isSelectionMode && editable) { controller.enableSelectionMode(); controller.select(entity.path); + onSelectionChanged(); } }, - onSaved: editable + onSaved: editable && !isSelectionMode ? (value) => context.read().add( PageRenamed( entity.path, @@ -441,6 +599,15 @@ class _PageEntityListTile extends StatelessWidget { : null, actions: editable && !isSelectionMode ? [ + MenuItemButton( + leadingIcon: const PhosphorIcon(PhosphorIconsLight.check), + onPressed: () { + controller.enableSelectionMode(); + controller.select(entity.path); + onSelectionChanged(); + }, + child: Text(AppLocalizations.of(context).select), + ), MenuItemButton( leadingIcon: const PhosphorIcon(PhosphorIconsLight.trash), onPressed: selected diff --git a/app/lib/widgets/multi_select.dart b/app/lib/widgets/multi_select.dart index 1abd7710797e..afadbc196369 100644 --- a/app/lib/widgets/multi_select.dart +++ b/app/lib/widgets/multi_select.dart @@ -1,196 +1,200 @@ -import 'package:flutter/material.dart'; - -/// Controller for managing multiple selection state. -class MultiSelectController extends ChangeNotifier { - final Set _selectedIds = {}; - bool _isSelectionMode = false; - - /// Returns an unmodifiable set of the currently selected ideas. - Set get selectedIds => Set.unmodifiable(_selectedIds); - - /// Returns true if selection mode is currently active. - bool get selectionMode => _isSelectionMode || _selectedIds.isNotEmpty; - - /// Manually enables selection mode without selecting an item. - void enableSelectionMode() { - if (!_isSelectionMode) { - _isSelectionMode = true; - notifyListeners(); - } - } - - /// Toggles the selection state of the given [id]. - void toggle(T id) { - if (_selectedIds.contains(id)) { - _selectedIds.remove(id); - } else { - _selectedIds.add(id); - } - if (_selectedIds.isEmpty) { - _isSelectionMode = false; - } - notifyListeners(); - } - - /// Selects the given [id]. - void select(T id) { - if (_selectedIds.add(id)) { - notifyListeners(); - } - } - - /// Deselects the given [id]. - void deselect(T id) { - if (_selectedIds.remove(id)) { - if (_selectedIds.isEmpty) { - _isSelectionMode = false; - } - notifyListeners(); - } - } - - /// Clears all selections and disables selection mode. - void clear() { - _selectedIds.clear(); - _isSelectionMode = false; - notifyListeners(); - } - - /// Selects all given [ids]. - void selectAll(Iterable ids) { - _selectedIds.addAll(ids); - notifyListeners(); - } -} - -/// An [InheritedWidget] that provides the [MultiSelectController] to its descendants. -class MultiSelectProvider - extends InheritedNotifier> { - const MultiSelectProvider({ - super.key, - required MultiSelectController controller, - required super.child, - }) : super(notifier: controller); - - static MultiSelectController of(BuildContext context) { - final provider = context - .dependOnInheritedWidgetOfExactType>(); - assert(provider != null, 'No MultiSelectProvider found in context'); - return provider!.notifier!; - } -} - -/// A region that manages a [MultiSelectController] and provides it to its subtree. -class MultiSelectRegion extends StatefulWidget { - final Widget Function( - BuildContext context, - MultiSelectController controller, - Widget? child, - ) - builder; - final Widget Function( - BuildContext context, - MultiSelectController controller, - )? - toolbarBuilder; - final Widget? child; - - const MultiSelectRegion({ - super.key, - required this.builder, - this.toolbarBuilder, - this.child, - }); - - @override - State> createState() => _MultiSelectRegionState(); -} - -class _MultiSelectRegionState extends State> { - late MultiSelectController _controller; - - @override - void initState() { - super.initState(); - _controller = MultiSelectController(); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return MultiSelectProvider( - controller: _controller, - child: ListenableBuilder( - listenable: _controller, - builder: (context, _) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (widget.toolbarBuilder != null && _controller.selectionMode) - widget.toolbarBuilder!(context, _controller), - Expanded( - child: widget.builder(context, _controller, widget.child), - ), - ], - ); - }, - ), - ); - } -} - -/// A convenience tile that interacts with the [MultiSelectController] provided by [MultiSelectRegion]. -class MultiSelectListTile extends StatelessWidget { - final T id; - final Widget title; - final Widget? subtitle; - final Widget? leading; - final VoidCallback? onTap; - final VoidCallback? onLongPress; - - const MultiSelectListTile({ - super.key, - required this.id, - required this.title, - this.subtitle, - this.leading, - this.onTap, - this.onLongPress, - }); - - @override - Widget build(BuildContext context) { - final controller = MultiSelectProvider.of(context); - final isSelected = controller.selectedIds.contains(id); - final selectionMode = controller.selectionMode; - - return ListTile( - title: title, - subtitle: subtitle, - leading: selectionMode - ? Checkbox(value: isSelected, onChanged: (_) => controller.toggle(id)) - : leading, - selected: isSelected, - onTap: () { - if (selectionMode) { - controller.toggle(id); - } else if (onTap != null) { - onTap!(); - } - }, - onLongPress: () { - if (!selectionMode) { - controller.enableSelectionMode(); - controller.toggle(id); - } else if (onLongPress != null) { - onLongPress!(); - } - }, - ); - } -} +import 'package:flutter/material.dart'; + +/// Controller for managing multiple selection state. +class MultiSelectController extends ChangeNotifier { + final Set _selectedIds = {}; + bool _isSelectionMode = false; + + /// Returns an unmodifiable set of the currently selected ideas. + Set get selectedIds => Set.unmodifiable(_selectedIds); + + /// Returns true if selection mode is currently active. + bool get selectionMode => _isSelectionMode || _selectedIds.isNotEmpty; + + /// Manually enables selection mode without selecting an item. + void enableSelectionMode() { + if (!_isSelectionMode) { + _isSelectionMode = true; + notifyListeners(); + } + } + + /// Toggles the selection state of the given [id]. + void toggle(T id) { + if (_selectedIds.contains(id)) { + _selectedIds.remove(id); + } else { + _selectedIds.add(id); + } + if (_selectedIds.isEmpty) { + _isSelectionMode = false; + } + notifyListeners(); + } + + /// Selects the given [id]. + void select(T id) { + if (_selectedIds.add(id)) { + notifyListeners(); + } + } + + /// Deselects the given [id]. + void deselect(T id) { + if (_selectedIds.remove(id)) { + if (_selectedIds.isEmpty) { + _isSelectionMode = false; + } + notifyListeners(); + } + } + + /// Clears all selections and disables selection mode. + void clear() { + _selectedIds.clear(); + _isSelectionMode = false; + notifyListeners(); + } + + /// Selects all given [ids]. + void selectAll(Iterable ids) { + _selectedIds.addAll(ids); + notifyListeners(); + } +} + +/// An [InheritedWidget] that provides the [MultiSelectController] to its descendants. +class MultiSelectProvider + extends InheritedNotifier> { + const MultiSelectProvider({ + super.key, + required MultiSelectController controller, + required super.child, + }) : super(notifier: controller); + + static MultiSelectController of(BuildContext context) { + final provider = context + .dependOnInheritedWidgetOfExactType>(); + assert(provider != null, 'No MultiSelectProvider found in context'); + return provider!.notifier!; + } +} + +/// A region that manages a [MultiSelectController] and provides it to its subtree. +class MultiSelectRegion extends StatefulWidget { + final Widget Function( + BuildContext context, + MultiSelectController controller, + Widget? child, + ) + builder; + final Widget Function( + BuildContext context, + MultiSelectController controller, + )? + toolbarBuilder; + final Widget? child; + final MultiSelectController? controller; + + const MultiSelectRegion({ + super.key, + required this.builder, + this.toolbarBuilder, + this.child, + this.controller, + }); + + @override + State> createState() => _MultiSelectRegionState(); +} + +class _MultiSelectRegionState extends State> { + late MultiSelectController _controller; + + @override + void initState() { + super.initState(); + _controller = widget.controller ?? MultiSelectController(); + } + + @override + void dispose() { + if (widget.controller == null) { + _controller.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MultiSelectProvider( + controller: _controller, + child: ListenableBuilder( + listenable: _controller, + builder: (context, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (widget.toolbarBuilder != null && _controller.selectionMode) + widget.toolbarBuilder!(context, _controller), + Expanded( + child: widget.builder(context, _controller, widget.child), + ), + ], + ); + }, + ), + ); + } +} + +/// A convenience tile that interacts with the [MultiSelectController] provided by [MultiSelectRegion]. +class MultiSelectListTile extends StatelessWidget { + final T id; + final Widget title; + final Widget? subtitle; + final Widget? leading; + final VoidCallback? onTap; + final VoidCallback? onLongPress; + + const MultiSelectListTile({ + super.key, + required this.id, + required this.title, + this.subtitle, + this.leading, + this.onTap, + this.onLongPress, + }); + + @override + Widget build(BuildContext context) { + final controller = MultiSelectProvider.of(context); + final isSelected = controller.selectedIds.contains(id); + final selectionMode = controller.selectionMode; + + return ListTile( + title: title, + subtitle: subtitle, + leading: selectionMode + ? Checkbox(value: isSelected, onChanged: (_) => controller.toggle(id)) + : leading, + selected: isSelected, + onTap: () { + if (selectionMode) { + controller.toggle(id); + } else if (onTap != null) { + onTap!(); + } + }, + onLongPress: () { + if (!selectionMode) { + controller.enableSelectionMode(); + controller.toggle(id); + } else if (onLongPress != null) { + onLongPress!(); + } + }, + ); + } +} diff --git a/app/test/views/navigator/pages_test.dart b/app/test/views/navigator/pages_test.dart index b28a462ff268..490ef989f216 100644 --- a/app/test/views/navigator/pages_test.dart +++ b/app/test/views/navigator/pages_test.dart @@ -85,6 +85,15 @@ void main() { expect(pages_dialog.parsePageSelection('5-3, 2', 6), [1, 2, 3, 4]); }); + test('formats selected pages as compact ranges', () { + expect( + pages_dialog.formatPageSelection([0, 1, 2, 4, 6, 7]), + '1-3, 5, 7-8', + ); + expect(pages_dialog.formatPageSelection([3]), '4'); + expect(pages_dialog.formatPageSelection([]), ''); + }); + test('rejects out of bounds and malformed input', () { expect(pages_dialog.parsePageSelection('0, 2', 4), isNull); expect(pages_dialog.parsePageSelection('2-', 4), isNull); From ff14843ff0c6a7d83d5d8dc1bc5a8f61488e4c0d Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 28 Jun 2026 10:21:04 +0200 Subject: [PATCH 018/117] Add show internal name --- app/lib/views/navigator/pages.dart | 70 +++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/app/lib/views/navigator/pages.dart b/app/lib/views/navigator/pages.dart index a2e333e95dce..3456ff98d0c5 100644 --- a/app/lib/views/navigator/pages.dart +++ b/app/lib/views/navigator/pages.dart @@ -85,6 +85,7 @@ class _PagesViewState extends State { MultiSelectController(); String? _rangeError; bool _updatingRangeText = false; + bool _showInternalPageNumbers = false; @override void dispose() { @@ -182,18 +183,35 @@ class _PagesViewState extends State { controller: _locationController, decoration: InputDecoration( labelText: AppLocalizations.of(context).location, - suffixIcon: IconButton( - icon: const PhosphorIcon(PhosphorIconsLight.arrowUp), - tooltip: AppLocalizations.of(context).goUp, - onPressed: () { - final paths = _locationController.text.split('/'); - if (paths.length <= 1) { - _locationController.text = ''; - return; - } - paths.removeLast(); - _locationController.text = paths.join('/'); - }, + suffixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(PhosphorIconsLight.listNumbers), + selectedIcon: const Icon( + PhosphorIconsFill.listNumbers, + ), + isSelected: _showInternalPageNumbers, + tooltip: AppLocalizations.of(context).pages, + onPressed: () => setState( + () => _showInternalPageNumbers = + !_showInternalPageNumbers, + ), + ), + IconButton( + icon: const Icon(PhosphorIconsLight.arrowUp), + tooltip: AppLocalizations.of(context).goUp, + onPressed: () { + final paths = _locationController.text.split('/'); + if (paths.length <= 1) { + _locationController.text = ''; + return; + } + paths.removeLast(); + _locationController.text = paths.join('/'); + }, + ), + ], ), border: const OutlineInputBorder(), ), @@ -253,6 +271,8 @@ class _PagesViewState extends State { return _PageEntityListTile( entity: entity, selected: entity.path == currentName, + showInternalPageNumber: + _showInternalPageNumbers, locationController: _locationController, controller: controller, data: state.data, @@ -523,6 +543,7 @@ class _PageEntityListTile extends StatelessWidget { const _PageEntityListTile({ required this.entity, required this.selected, + required this.showInternalPageNumber, required this.locationController, required this.controller, required this.data, @@ -533,6 +554,7 @@ class _PageEntityListTile extends StatelessWidget { final PageEntity entity; final bool selected; + final bool showInternalPageNumber; final NoteData data; final int index; final TextEditingController locationController; @@ -570,6 +592,13 @@ class _PageEntityListTile extends StatelessWidget { ), textFormatter: (v) => v.isEmpty ? AppLocalizations.of(context).untitled : v, + subtitle: showInternalPageNumber && editable + ? Text( + AppLocalizations.of( + context, + ).pageIndex((data.getPageIndex(entity.path) ?? index) + 1), + ) + : null, onTap: () { if (isSelectionMode && editable) { controller.toggle(entity.path); @@ -608,6 +637,23 @@ class _PageEntityListTile extends StatelessWidget { }, child: Text(AppLocalizations.of(context).select), ), + MenuItemButton( + leadingIcon: const PhosphorIcon(PhosphorIconsLight.copy), + onPressed: () { + final page = data.getPage(entity.path); + if (page == null) return; + context.read().add( + PagesAdded([ + PageAddedDetails( + page: page, + name: entity.name, + index: (data.getPageIndex(entity.path) ?? index) + 1, + ), + ]), + ); + }, + child: Text(AppLocalizations.of(context).duplicate), + ), MenuItemButton( leadingIcon: const PhosphorIcon(PhosphorIconsLight.trash), onPressed: selected From a67df8373bcc3201a46dc91ff33c6f72c74a3d33 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 28 Jun 2026 10:36:02 +0200 Subject: [PATCH 019/117] Add support for showing areas from all pages --- api/lib/src/protocol/event.dart | 3 +- api/lib/src/protocol/event.freezed.dart | 10 +- api/lib/src/protocol/event.g.dart | 9 +- app/lib/bloc/document_bloc.dart | 40 ++- app/lib/dialogs/area/context.dart | 6 +- app/lib/selections/area.dart | 8 +- app/lib/views/navigator/areas.dart | 144 ++++++--- app/lib/views/navigator/pages.dart | 4 +- app/lib/widgets/multi_select.dart | 400 ++++++++++++------------ app/test/bloc/document_bloc_test.dart | 28 ++ 10 files changed, 395 insertions(+), 257 deletions(-) diff --git a/api/lib/src/protocol/event.dart b/api/lib/src/protocol/event.dart index 4914901362d5..a461061dd7b9 100644 --- a/api/lib/src/protocol/event.dart +++ b/api/lib/src/protocol/event.dart @@ -157,7 +157,8 @@ sealed class DocumentEvent extends ReplayEvent with _$DocumentEvent { const factory DocumentEvent.areasDuplicated(Area area, List pages) = AreasDuplicated; - const factory DocumentEvent.areasRemoved(List areas) = AreasRemoved; + const factory DocumentEvent.areasRemoved(List areas) = + AreasRemoved; const factory DocumentEvent.areaChanged( String name, diff --git a/api/lib/src/protocol/event.freezed.dart b/api/lib/src/protocol/event.freezed.dart index c305e10ca97d..9131e37f3287 100644 --- a/api/lib/src/protocol/event.freezed.dart +++ b/api/lib/src/protocol/event.freezed.dart @@ -3424,11 +3424,11 @@ $AreaCopyWith<$Res> get area { @JsonSerializable() class AreasRemoved extends DocumentEvent { - const AreasRemoved(final List areas, {final String? $type}): _areas = areas,$type = $type ?? 'areasRemoved',super._(); + const AreasRemoved(final List areas, {final String? $type}): _areas = areas,$type = $type ?? 'areasRemoved',super._(); factory AreasRemoved.fromJson(Map json) => _$AreasRemovedFromJson(json); - final List _areas; - List get areas { + final List _areas; + List get areas { if (_areas is EqualUnmodifiableListView) return _areas; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(_areas); @@ -3472,7 +3472,7 @@ abstract mixin class $AreasRemovedCopyWith<$Res> implements $DocumentEventCopyWi factory $AreasRemovedCopyWith(AreasRemoved value, $Res Function(AreasRemoved) _then) = _$AreasRemovedCopyWithImpl; @useResult $Res call({ - List areas + List areas }); @@ -3492,7 +3492,7 @@ class _$AreasRemovedCopyWithImpl<$Res> @pragma('vm:prefer-inline') $Res call({Object? areas = null,}) { return _then(AreasRemoved( null == areas ? _self._areas : areas // ignore: cast_nullable_to_non_nullable -as List, +as List, )); } diff --git a/api/lib/src/protocol/event.g.dart b/api/lib/src/protocol/event.g.dart index a4d1bec2d4e0..d33315a0302a 100644 --- a/api/lib/src/protocol/event.g.dart +++ b/api/lib/src/protocol/event.g.dart @@ -478,12 +478,17 @@ Map _$AreasDuplicatedToJson(AreasDuplicated instance) => }; AreasRemoved _$AreasRemovedFromJson(Map json) => AreasRemoved( - (json['areas'] as List).map((e) => e as String).toList(), + (json['areas'] as List) + .map((e) => AreaPreset.fromJson(Map.from(e as Map))) + .toList(), $type: json['type'] as String?, ); Map _$AreasRemovedToJson(AreasRemoved instance) => - {'areas': instance.areas, 'type': instance.$type}; + { + 'areas': instance.areas.map((e) => e.toJson()).toList(), + 'type': instance.$type, + }; AreaChanged _$AreaChangedFromJson(Map json) => AreaChanged( json['name'] as String, diff --git a/app/lib/bloc/document_bloc.dart b/app/lib/bloc/document_bloc.dart index ea664e1bc776..4cb303a7965e 100644 --- a/app/lib/bloc/document_bloc.dart +++ b/app/lib/bloc/document_bloc.dart @@ -1249,20 +1249,42 @@ class DocumentBloc extends ReplayBloc { final current = state; if (current is! DocumentLoadSuccess) return; if (!(embedding?.editable ?? true)) return; - final areas = List.from(current.page.areas) - ..removeWhere((e) => event.areas.contains(e.name)); - final currentPage = current.page.copyWith(areas: areas); + var data = current.data.setPage(current.page, current.pageName).$1; + var currentPage = current.page; + var currentAreaName = current.currentAreaName; + var currentPageChanged = false; var shouldRepaint = false; - for (var element in currentIndexCubit.renderers) { - if (areas.contains(element.area) && - element.onAreaUpdate(current.data, currentPage, null)) { - shouldRepaint = true; + final areasByPage = >{}; + for (final area in event.areas) { + areasByPage.putIfAbsent(area.page, () => {}).add(area.name); + } + for (final entry in areasByPage.entries) { + final page = data.getPage(entry.key); + if (page == null) continue; + final areas = List.from(page.areas) + ..removeWhere((e) => entry.value.contains(e.name)); + final updatedPage = page.copyWith(areas: areas); + data = data.setPage(updatedPage, entry.key).$1; + if (entry.key == current.pageName) { + currentPage = updatedPage; + currentPageChanged = true; + if (entry.value.contains(currentAreaName)) currentAreaName = ''; + for (var element in currentIndexCubit.renderers) { + if (areas.contains(element.area) && + element.onAreaUpdate(current.data, currentPage, null)) { + shouldRepaint = true; + } + } } } _saveState( emit, - state: current.copyWith(page: currentPage), - shouldRefresh: () => true, + state: current.copyWith( + data: data, + page: currentPage, + currentAreaName: currentAreaName, + ), + shouldRefresh: () => currentPageChanged, reset: shouldRepaint, ); }); diff --git a/app/lib/dialogs/area/context.dart b/app/lib/dialogs/area/context.dart index 246e0c368c72..a59de84dc2c1 100644 --- a/app/lib/dialogs/area/context.dart +++ b/app/lib/dialogs/area/context.dart @@ -24,8 +24,10 @@ ContextMenuBuilder buildAreaContextMenu( SettingsCubit settingsCubit, { bool pop = true, bool includeRenameAndEnterArea = true, + String? pageName, }) => (context) { final cubit = bloc.currentIndexCubit; + final areaPageName = pageName ?? state.pageName; return [ if (includeRenameAndEnterArea) ...[ ContextMenuItem( @@ -89,7 +91,9 @@ ContextMenuBuilder buildAreaContextMenu( label: AppLocalizations.of(context).delete, onPressed: () { if (pop) Navigator.of(context).pop(); - bloc.add(AreasRemoved([area.name])); + bloc.add( + AreasRemoved([AreaPreset(page: areaPageName, name: area.name)]), + ); }, ), ContextMenuItem( diff --git a/app/lib/selections/area.dart b/app/lib/selections/area.dart index 0b9fef1d7d1b..8e206d1f94d4 100644 --- a/app/lib/selections/area.dart +++ b/app/lib/selections/area.dart @@ -96,8 +96,14 @@ class AreaSelection extends Selection { @override void onDelete(BuildContext context) { + final state = context.read().state; + if (state is! DocumentLoadSuccess) return; context.read().add( - AreasRemoved(super.selected.map((e) => e.name).toList()), + AreasRemoved( + super.selected + .map((e) => AreaPreset(page: state.pageName, name: e.name)) + .toList(), + ), ); } } diff --git a/app/lib/views/navigator/areas.dart b/app/lib/views/navigator/areas.dart index f6ba0f244fc6..b2bb9a5e439d 100644 --- a/app/lib/views/navigator/areas.dart +++ b/app/lib/views/navigator/areas.dart @@ -20,6 +20,13 @@ import '../../dialogs/delete.dart'; import '../../widgets/multi_select.dart'; import '../../widgets/editable_list_tile.dart'; +typedef _AreaEntry = ({ + Area area, + String pageName, + String pageDisplayName, + bool isCurrentPage, +}); + class AreasView extends StatefulWidget { const AreasView({super.key}); @@ -30,6 +37,7 @@ class AreasView extends StatefulWidget { class _AreasViewState extends State { String _currentGroup = ''; final TextEditingController _searchController = TextEditingController(); + bool _showAllPages = false; List _getAreasInGroup(List areas) { return areas.where((area) => area.group == _currentGroup).toList(); @@ -95,23 +103,37 @@ class _AreasViewState extends State { DocumentLoadSuccess state, Rect viewportRect, Area? current, - Area area, { - required MultiSelectController controller, + _AreaEntry entry, { + required MultiSelectController controller, List folderPath = const [], }) { - final selected = current?.name == area.name; + final area = entry.area; + final selectionId = AreaPreset(page: entry.pageName, name: area.name); + final selected = entry.isCurrentPage && current?.name == area.name; final isSelectionMode = controller.selectionMode; final isSelected = isSelectionMode - ? controller.selectedIds.contains(area.name) + ? controller.selectedIds.contains(selectionId) : selected; + void navigateToArea() { + if (!entry.isCurrentPage) { + bloc.add(PageChanged(entry.pageName)); + } + context.read().teleportToArea( + area, + viewport.toSize(), + viewport.resolution, + ); + bloc.add(CurrentAreaChanged(area.name)); + } + return EditableListTile( initialValue: area.shortName, - key: ValueKey(area.name), + key: ValueKey(selectionId), leading: isSelectionMode ? Checkbox( value: isSelected, - onChanged: (value) => controller.toggle(area.name), + onChanged: (value) => controller.toggle(selectionId), ) : IconButton( icon: PhosphorIcon( @@ -124,12 +146,7 @@ class _AreasViewState extends State { bloc.add(CurrentAreaChanged('')); return; } - context.read().teleportToArea( - area, - viewport.toSize(), - viewport.resolution, - ); - bloc.add(CurrentAreaChanged(area.name)); + navigateToArea(); }, tooltip: selected ? AppLocalizations.of(context).exitArea @@ -137,25 +154,23 @@ class _AreasViewState extends State { ), onTap: () { if (isSelectionMode) { - controller.toggle(area.name); + controller.toggle(selectionId); return; } - context.read().teleportToArea( - area, - viewport.toSize(), - viewport.resolution, - ); - bloc.add(CurrentAreaChanged(area.name)); + navigateToArea(); }, onLongPress: () { if (!isSelectionMode) { controller.enableSelectionMode(); - controller.select(area.name); + controller.select(selectionId); } }, onSaved: (value) { final trimmed = value.trim(); if (trimmed.isEmpty) return; + if (!entry.isCurrentPage) { + bloc.add(PageChanged(entry.pageName)); + } final nextName = trimmed.contains('/') ? trimmed : folderPath.isEmpty @@ -165,9 +180,10 @@ class _AreasViewState extends State { AreaChanged(area.name, area.copyWith(name: nextName)), ); }, - selected: current == null && !isSelectionMode + selected: entry.isCurrentPage && current == null && !isSelectionMode ? area.rect.overlaps(viewportRect) : isSelected, + subtitle: _showAllPages ? Text(entry.pageDisplayName) : null, actions: isSelectionMode ? null : [ @@ -178,6 +194,7 @@ class _AreasViewState extends State { context.read(), pop: false, includeRenameAndEnterArea: false, + pageName: entry.pageName, )(context).map((e) => buildMenuItem(context, e, false, false)), ], ); @@ -210,12 +227,39 @@ class _AreasViewState extends State { return BlocBuilder( buildWhen: (previous, current) => previous.page?.areas != current.page?.areas || - previous.currentArea != current.currentArea, + previous.currentArea != current.currentArea || + (_showAllPages && + previous is DocumentLoadSuccess && + current is DocumentLoadSuccess && + previous.data != current.data), builder: (context, state) { if (state is! DocumentLoadSuccess) { return const SizedBox.shrink(); } final current = state.currentArea; + final pagesWithNames = state.data.getPagesWithNames(); + final currentPageDisplayName = pagesWithNames + .where((page) => page.$2 == state.pageName) + .map((page) => page.$1) + .firstOrNull; + final allPageEntries = pagesWithNames.expand((pageNames) { + final realPageName = pageNames.$2; + final page = realPageName == state.pageName + ? state.page + : state.data.getPage(realPageName); + if (page == null) return const <_AreaEntry>[]; + final displayName = pageNames.$1.isEmpty + ? AppLocalizations.of(context).page + : pageNames.$1; + return page.areas.map( + (area) => ( + area: area, + pageName: realPageName, + pageDisplayName: displayName, + isCurrentPage: realPageName == state.pageName, + ), + ); + }).toList(); final currentIndexCubit = context.read(); @@ -243,7 +287,7 @@ class _AreasViewState extends State { ); } - if (state.page.areas.isEmpty) { + if (!_showAllPages && state.page.areas.isEmpty) { return AreasInitializationView( onCreate: _createArea, insideDocument: true, @@ -251,7 +295,9 @@ class _AreasViewState extends State { ); } - final allSubgroups = _getSubgroups(state.page.areas); + final allSubgroups = _showAllPages + ? const [] + : _getSubgroups(state.page.areas); final areasInGroup = _getAreasInGroup(state.page.areas); return Column( @@ -265,10 +311,21 @@ class _AreasViewState extends State { leading: const PhosphorIcon( PhosphorIconsLight.magnifyingGlass, ), + trailing: [ + IconButton( + icon: const PhosphorIcon(PhosphorIconsLight.files), + isSelected: _showAllPages, + tooltip: AppLocalizations.of(context).pages, + onPressed: () => setState(() { + _showAllPages = !_showAllPages; + if (_showAllPages) _currentGroup = ''; + }), + ), + ], ), ), const Divider(), - if (_currentGroup.isNotEmpty) ...[ + if (!_showAllPages && _currentGroup.isNotEmpty) ...[ Padding( padding: const EdgeInsets.symmetric( horizontal: 4, @@ -308,7 +365,7 @@ class _AreasViewState extends State { Expanded( child: Material( type: MaterialType.transparency, - child: MultiSelectRegion( + child: MultiSelectRegion( toolbarBuilder: (context, controller) => Padding( padding: const EdgeInsets.symmetric( horizontal: 8.0, @@ -345,13 +402,30 @@ class _AreasViewState extends State { builder: (context, child) { final search = _searchController.text .toLowerCase(); - final areas = areasInGroup - .where( - (area) => area.name.toLowerCase().contains( - search, - ), - ) - .toList(); + final areaEntries = _showAllPages + ? allPageEntries + : areasInGroup + .map( + (area) => ( + area: area, + pageName: state.pageName, + pageDisplayName: + currentPageDisplayName ?? + AppLocalizations.of( + context, + ).page, + isCurrentPage: true, + ), + ) + .toList(); + final areas = areaEntries.where((entry) { + final areaName = entry.area.name.toLowerCase(); + final pageName = entry.pageDisplayName + .toLowerCase(); + return areaName.contains(search) || + (_showAllPages && + pageName.contains(search)); + }).toList(); final subgroups = allSubgroups .where( (group) => @@ -375,13 +449,13 @@ class _AreasViewState extends State { ), ), ...areas.map( - (area) => buildAreaTile( + (entry) => buildAreaTile( bloc, viewport, state, viewportRect, current, - area, + entry, controller: controller, folderPath: _currentGroup.isEmpty ? [] diff --git a/app/lib/views/navigator/pages.dart b/app/lib/views/navigator/pages.dart index 3456ff98d0c5..9c58ea5ee0cc 100644 --- a/app/lib/views/navigator/pages.dart +++ b/app/lib/views/navigator/pages.dart @@ -188,9 +188,7 @@ class _PagesViewState extends State { children: [ IconButton( icon: const Icon(PhosphorIconsLight.listNumbers), - selectedIcon: const Icon( - PhosphorIconsFill.listNumbers, - ), + selectedIcon: const Icon(PhosphorIconsFill.listNumbers), isSelected: _showInternalPageNumbers, tooltip: AppLocalizations.of(context).pages, onPressed: () => setState( diff --git a/app/lib/widgets/multi_select.dart b/app/lib/widgets/multi_select.dart index afadbc196369..db297556f849 100644 --- a/app/lib/widgets/multi_select.dart +++ b/app/lib/widgets/multi_select.dart @@ -1,200 +1,200 @@ -import 'package:flutter/material.dart'; - -/// Controller for managing multiple selection state. -class MultiSelectController extends ChangeNotifier { - final Set _selectedIds = {}; - bool _isSelectionMode = false; - - /// Returns an unmodifiable set of the currently selected ideas. - Set get selectedIds => Set.unmodifiable(_selectedIds); - - /// Returns true if selection mode is currently active. - bool get selectionMode => _isSelectionMode || _selectedIds.isNotEmpty; - - /// Manually enables selection mode without selecting an item. - void enableSelectionMode() { - if (!_isSelectionMode) { - _isSelectionMode = true; - notifyListeners(); - } - } - - /// Toggles the selection state of the given [id]. - void toggle(T id) { - if (_selectedIds.contains(id)) { - _selectedIds.remove(id); - } else { - _selectedIds.add(id); - } - if (_selectedIds.isEmpty) { - _isSelectionMode = false; - } - notifyListeners(); - } - - /// Selects the given [id]. - void select(T id) { - if (_selectedIds.add(id)) { - notifyListeners(); - } - } - - /// Deselects the given [id]. - void deselect(T id) { - if (_selectedIds.remove(id)) { - if (_selectedIds.isEmpty) { - _isSelectionMode = false; - } - notifyListeners(); - } - } - - /// Clears all selections and disables selection mode. - void clear() { - _selectedIds.clear(); - _isSelectionMode = false; - notifyListeners(); - } - - /// Selects all given [ids]. - void selectAll(Iterable ids) { - _selectedIds.addAll(ids); - notifyListeners(); - } -} - -/// An [InheritedWidget] that provides the [MultiSelectController] to its descendants. -class MultiSelectProvider - extends InheritedNotifier> { - const MultiSelectProvider({ - super.key, - required MultiSelectController controller, - required super.child, - }) : super(notifier: controller); - - static MultiSelectController of(BuildContext context) { - final provider = context - .dependOnInheritedWidgetOfExactType>(); - assert(provider != null, 'No MultiSelectProvider found in context'); - return provider!.notifier!; - } -} - -/// A region that manages a [MultiSelectController] and provides it to its subtree. -class MultiSelectRegion extends StatefulWidget { - final Widget Function( - BuildContext context, - MultiSelectController controller, - Widget? child, - ) - builder; - final Widget Function( - BuildContext context, - MultiSelectController controller, - )? - toolbarBuilder; - final Widget? child; - final MultiSelectController? controller; - - const MultiSelectRegion({ - super.key, - required this.builder, - this.toolbarBuilder, - this.child, - this.controller, - }); - - @override - State> createState() => _MultiSelectRegionState(); -} - -class _MultiSelectRegionState extends State> { - late MultiSelectController _controller; - - @override - void initState() { - super.initState(); - _controller = widget.controller ?? MultiSelectController(); - } - - @override - void dispose() { - if (widget.controller == null) { - _controller.dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return MultiSelectProvider( - controller: _controller, - child: ListenableBuilder( - listenable: _controller, - builder: (context, _) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (widget.toolbarBuilder != null && _controller.selectionMode) - widget.toolbarBuilder!(context, _controller), - Expanded( - child: widget.builder(context, _controller, widget.child), - ), - ], - ); - }, - ), - ); - } -} - -/// A convenience tile that interacts with the [MultiSelectController] provided by [MultiSelectRegion]. -class MultiSelectListTile extends StatelessWidget { - final T id; - final Widget title; - final Widget? subtitle; - final Widget? leading; - final VoidCallback? onTap; - final VoidCallback? onLongPress; - - const MultiSelectListTile({ - super.key, - required this.id, - required this.title, - this.subtitle, - this.leading, - this.onTap, - this.onLongPress, - }); - - @override - Widget build(BuildContext context) { - final controller = MultiSelectProvider.of(context); - final isSelected = controller.selectedIds.contains(id); - final selectionMode = controller.selectionMode; - - return ListTile( - title: title, - subtitle: subtitle, - leading: selectionMode - ? Checkbox(value: isSelected, onChanged: (_) => controller.toggle(id)) - : leading, - selected: isSelected, - onTap: () { - if (selectionMode) { - controller.toggle(id); - } else if (onTap != null) { - onTap!(); - } - }, - onLongPress: () { - if (!selectionMode) { - controller.enableSelectionMode(); - controller.toggle(id); - } else if (onLongPress != null) { - onLongPress!(); - } - }, - ); - } -} +import 'package:flutter/material.dart'; + +/// Controller for managing multiple selection state. +class MultiSelectController extends ChangeNotifier { + final Set _selectedIds = {}; + bool _isSelectionMode = false; + + /// Returns an unmodifiable set of the currently selected ideas. + Set get selectedIds => Set.unmodifiable(_selectedIds); + + /// Returns true if selection mode is currently active. + bool get selectionMode => _isSelectionMode || _selectedIds.isNotEmpty; + + /// Manually enables selection mode without selecting an item. + void enableSelectionMode() { + if (!_isSelectionMode) { + _isSelectionMode = true; + notifyListeners(); + } + } + + /// Toggles the selection state of the given [id]. + void toggle(T id) { + if (_selectedIds.contains(id)) { + _selectedIds.remove(id); + } else { + _selectedIds.add(id); + } + if (_selectedIds.isEmpty) { + _isSelectionMode = false; + } + notifyListeners(); + } + + /// Selects the given [id]. + void select(T id) { + if (_selectedIds.add(id)) { + notifyListeners(); + } + } + + /// Deselects the given [id]. + void deselect(T id) { + if (_selectedIds.remove(id)) { + if (_selectedIds.isEmpty) { + _isSelectionMode = false; + } + notifyListeners(); + } + } + + /// Clears all selections and disables selection mode. + void clear() { + _selectedIds.clear(); + _isSelectionMode = false; + notifyListeners(); + } + + /// Selects all given [ids]. + void selectAll(Iterable ids) { + _selectedIds.addAll(ids); + notifyListeners(); + } +} + +/// An [InheritedWidget] that provides the [MultiSelectController] to its descendants. +class MultiSelectProvider + extends InheritedNotifier> { + const MultiSelectProvider({ + super.key, + required MultiSelectController controller, + required super.child, + }) : super(notifier: controller); + + static MultiSelectController of(BuildContext context) { + final provider = context + .dependOnInheritedWidgetOfExactType>(); + assert(provider != null, 'No MultiSelectProvider found in context'); + return provider!.notifier!; + } +} + +/// A region that manages a [MultiSelectController] and provides it to its subtree. +class MultiSelectRegion extends StatefulWidget { + final Widget Function( + BuildContext context, + MultiSelectController controller, + Widget? child, + ) + builder; + final Widget Function( + BuildContext context, + MultiSelectController controller, + )? + toolbarBuilder; + final Widget? child; + final MultiSelectController? controller; + + const MultiSelectRegion({ + super.key, + required this.builder, + this.toolbarBuilder, + this.child, + this.controller, + }); + + @override + State> createState() => _MultiSelectRegionState(); +} + +class _MultiSelectRegionState extends State> { + late MultiSelectController _controller; + + @override + void initState() { + super.initState(); + _controller = widget.controller ?? MultiSelectController(); + } + + @override + void dispose() { + if (widget.controller == null) { + _controller.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MultiSelectProvider( + controller: _controller, + child: ListenableBuilder( + listenable: _controller, + builder: (context, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (widget.toolbarBuilder != null && _controller.selectionMode) + widget.toolbarBuilder!(context, _controller), + Expanded( + child: widget.builder(context, _controller, widget.child), + ), + ], + ); + }, + ), + ); + } +} + +/// A convenience tile that interacts with the [MultiSelectController] provided by [MultiSelectRegion]. +class MultiSelectListTile extends StatelessWidget { + final T id; + final Widget title; + final Widget? subtitle; + final Widget? leading; + final VoidCallback? onTap; + final VoidCallback? onLongPress; + + const MultiSelectListTile({ + super.key, + required this.id, + required this.title, + this.subtitle, + this.leading, + this.onTap, + this.onLongPress, + }); + + @override + Widget build(BuildContext context) { + final controller = MultiSelectProvider.of(context); + final isSelected = controller.selectedIds.contains(id); + final selectionMode = controller.selectionMode; + + return ListTile( + title: title, + subtitle: subtitle, + leading: selectionMode + ? Checkbox(value: isSelected, onChanged: (_) => controller.toggle(id)) + : leading, + selected: isSelected, + onTap: () { + if (selectionMode) { + controller.toggle(id); + } else if (onTap != null) { + onTap!(); + } + }, + onLongPress: () { + if (!selectionMode) { + controller.enableSelectionMode(); + controller.toggle(id); + } else if (onLongPress != null) { + onLongPress!(); + } + }, + ); + } +} diff --git a/app/test/bloc/document_bloc_test.dart b/app/test/bloc/document_bloc_test.dart index 793d8fe8849e..7147df031560 100644 --- a/app/test/bloc/document_bloc_test.dart +++ b/app/test/bloc/document_bloc_test.dart @@ -284,6 +284,34 @@ void main() { expect(state.data.getPage(secondPageName)?.areas, [area]); }); + test('removing areas can target multiple pages', () async { + final initialState = bloc.state as DocumentLoadSuccess; + final pages = initialState.data.getPages(true); + final firstPageName = pages.firstWhere((name) => name.endsWith('.Page 1')); + final secondPageName = pages.firstWhere((name) => name.endsWith('.Page 2')); + const area = Area( + name: 'Shared area', + width: 100, + height: 80, + position: Point(10, 20), + ); + + bloc.add(AreasDuplicated(area, ['Page 1', secondPageName])); + await _settleBlocEvents(); + bloc.add( + AreasRemoved([ + AreaPreset(page: firstPageName, name: area.name), + AreaPreset(page: secondPageName, name: area.name), + ]), + ); + await _settleBlocEvents(); + + final state = bloc.state as DocumentLoadSuccess; + expect(state.data.getPage(firstPageName)?.areas, isEmpty); + expect(state.data.getPage(secondPageName)?.areas, isEmpty); + expect(state.page.areas, isEmpty); + }); + test('unmatched single tool change does not overwrite active tool', () async { final activeTool = PenTool(id: 'active-tool'); final otherTool = PenTool(id: 'other-tool'); From 3a9ef0f488e06127294509268952be009d39a649 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 28 Jun 2026 10:41:13 +0200 Subject: [PATCH 020/117] Only show page selection text field on internal pages, remove rename on areas on selection --- app/lib/views/navigator/areas.dart | 1 + app/lib/views/navigator/pages.dart | 152 +++++++++++++++++------------ 2 files changed, 91 insertions(+), 62 deletions(-) diff --git a/app/lib/views/navigator/areas.dart b/app/lib/views/navigator/areas.dart index b2bb9a5e439d..1064337c178d 100644 --- a/app/lib/views/navigator/areas.dart +++ b/app/lib/views/navigator/areas.dart @@ -130,6 +130,7 @@ class _AreasViewState extends State { return EditableListTile( initialValue: area.shortName, key: ValueKey(selectionId), + showEditIcon: !isSelectionMode, leading: isSelectionMode ? Checkbox( value: isSelected, diff --git a/app/lib/views/navigator/pages.dart b/app/lib/views/navigator/pages.dart index 9c58ea5ee0cc..cd5cf94b8298 100644 --- a/app/lib/views/navigator/pages.dart +++ b/app/lib/views/navigator/pages.dart @@ -106,6 +106,48 @@ class _PagesViewState extends State { _updatingRangeText = false; } + void _toggleInternalPageNumbers() { + setState(() { + _showInternalPageNumbers = !_showInternalPageNumbers; + if (!_showInternalPageNumbers) { + _rangeError = null; + _setRangeText(''); + } + }); + } + + void _clearSelection() { + setState(() { + _rangeError = null; + _selectionController.clear(); + _setRangeText(''); + }); + } + + void _selectPageIndexes( + Iterable indexes, + List selectablePages, + ) { + _selectionController.clear(); + _selectionController.selectAll( + indexes.map((index) => selectablePages[index].path), + ); + } + + void _toggleAllPages(List selectablePages) { + final everythingSelected = + _selectionController.selectedIds.length == selectablePages.length; + setState(() { + _rangeError = null; + _selectionController.clear(); + if (!everythingSelected) { + _selectionController.selectAll( + selectablePages.map((entity) => entity.path), + ); + } + }); + } + void _syncRangeTextFromSelection( MultiSelectController controller, List selectablePages, @@ -135,16 +177,28 @@ class _PagesViewState extends State { return; } _rangeError = null; - _selectionController.clear(); - _selectionController.selectAll( - selectedIndexes.map((index) => selectablePages[index].path), - ); + _selectPageIndexes(selectedIndexes, selectablePages); if (normalizeText) { _setRangeText(pages_dialog.formatPageSelection(selectedIndexes)); } }); } + Future _deleteSelectedPages(BuildContext context) async { + if (_selectionController.selectedIds.isEmpty) return; + final result = await showDialog( + context: context, + builder: (context) => const DeleteDialog(), + ); + if (result != true) return; + if (!context.mounted) return; + final bloc = context.read(); + for (final id in _selectionController.selectedIds) { + bloc.add(PageRemoved(id)); + } + _clearSelection(); + } + @override Widget build(BuildContext context) { return BlocBuilder( @@ -191,10 +245,7 @@ class _PagesViewState extends State { selectedIcon: const Icon(PhosphorIconsFill.listNumbers), isSelected: _showInternalPageNumbers, tooltip: AppLocalizations.of(context).pages, - onPressed: () => setState( - () => _showInternalPageNumbers = - !_showInternalPageNumbers, - ), + onPressed: _toggleInternalPageNumbers, ), IconButton( icon: const Icon(PhosphorIconsLight.arrowUp), @@ -298,12 +349,15 @@ class _PagesViewState extends State { listenable: _selectionController, builder: (context, child) { if (_selectionController.selectionMode) { - _syncRangeTextFromSelection( - _selectionController, - selectablePages, - ); + if (_showInternalPageNumbers) { + _syncRangeTextFromSelection( + _selectionController, + selectablePages, + ); + } return _PagesSelectionBar( controller: _selectionController, + showRangeField: _showInternalPageNumbers, rangeController: _rangeController, rangeFocusNode: _rangeFocusNode, rangeError: _rangeError, @@ -315,44 +369,12 @@ class _PagesViewState extends State { selectablePages, normalizeText: true, ), - onClear: () { - setState(() { - _rangeError = null; - _selectionController.clear(); - _setRangeText(''); - }); - }, - onSelectAllChanged: () { - final everythingSelected = - _selectionController.selectedIds.length == - selectablePages.length; - setState(() { - _rangeError = null; - _selectionController.clear(); - if (!everythingSelected) { - _selectionController.selectAll( - selectablePages.map((entity) => entity.path), - ); - } - }); - }, + onClear: _clearSelection, + onSelectAllChanged: () => + _toggleAllPages(selectablePages), onDelete: _selectionController.selectedIds.isEmpty ? null - : () async { - final result = await showDialog( - context: context, - builder: (context) => const DeleteDialog(), - ); - if (result != true) return; - if (!context.mounted) return; - final bloc = context.read(); - for (final id - in _selectionController.selectedIds) { - bloc.add(PageRemoved(id)); - } - _selectionController.clear(); - _setRangeText(''); - }, + : () => _deleteSelectedPages(context), ); } return _PagesCreateBar(index: index, onAddPage: addPage); @@ -370,6 +392,7 @@ class _PagesViewState extends State { class _PagesSelectionBar extends StatelessWidget { const _PagesSelectionBar({ required this.controller, + required this.showRangeField, required this.rangeController, required this.rangeFocusNode, required this.rangeError, @@ -382,6 +405,7 @@ class _PagesSelectionBar extends StatelessWidget { }); final MultiSelectController controller; + final bool showRangeField; final TextEditingController rangeController; final FocusNode rangeFocusNode; final String? rangeError; @@ -435,21 +459,25 @@ class _PagesSelectionBar extends StatelessWidget { ), ], ), - const SizedBox(height: 6), - TextField( - controller: rangeController, - focusNode: rangeFocusNode, - decoration: InputDecoration( - prefixIcon: const PhosphorIcon(PhosphorIconsLight.listNumbers), - labelText: loc.pages, - hintText: '1-3, 5', - errorText: rangeError, - filled: true, - isDense: true, + if (showRangeField) ...[ + const SizedBox(height: 6), + TextField( + controller: rangeController, + focusNode: rangeFocusNode, + decoration: InputDecoration( + prefixIcon: const PhosphorIcon( + PhosphorIconsLight.listNumbers, + ), + labelText: loc.pages, + hintText: '1-3, 5', + errorText: rangeError, + filled: true, + isDense: true, + ), + onChanged: onRangeChanged, + onSubmitted: onRangeSubmitted, ), - onChanged: onRangeChanged, - onSubmitted: onRangeSubmitted, - ), + ], ], ), ), From 2632485546ff720d212ed4c75aa17c731c713867 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 28 Jun 2026 18:03:36 +0200 Subject: [PATCH 021/117] Add apply areas to templates, disable create areas if template already has areas --- api/lib/src/protocol/event.dart | 3 +- api/lib/src/protocol/event.freezed.dart | 10 +-- api/lib/src/protocol/event.g.dart | 2 +- app/lib/bloc/document_bloc.dart | 82 ++++++++++++++++--------- app/lib/cubits/current_index.dart | 2 +- app/lib/dialogs/template.dart | 41 ++++++++++++- app/lib/handlers/area.dart | 12 ++-- app/lib/handlers/import.dart | 6 +- app/lib/l10n/app_en.arb | 1 + app/lib/services/import.dart | 2 +- app/lib/views/navigator/areas.dart | 14 +++-- 11 files changed, 123 insertions(+), 52 deletions(-) diff --git a/api/lib/src/protocol/event.dart b/api/lib/src/protocol/event.dart index a461061dd7b9..23912248c22c 100644 --- a/api/lib/src/protocol/event.dart +++ b/api/lib/src/protocol/event.dart @@ -152,7 +152,8 @@ sealed class DocumentEvent extends ReplayEvent with _$DocumentEvent { String collection, ) = ElementsCollectionChanged; - const factory DocumentEvent.areasCreated(List areas) = AreasCreated; + const factory DocumentEvent.areasCreated(List areas) = + AreasCreated; const factory DocumentEvent.areasDuplicated(Area area, List pages) = AreasDuplicated; diff --git a/api/lib/src/protocol/event.freezed.dart b/api/lib/src/protocol/event.freezed.dart index 9131e37f3287..b532f553c51c 100644 --- a/api/lib/src/protocol/event.freezed.dart +++ b/api/lib/src/protocol/event.freezed.dart @@ -3255,11 +3255,11 @@ as String, @JsonSerializable() class AreasCreated extends DocumentEvent { - const AreasCreated(final List areas, {final String? $type}): _areas = areas,$type = $type ?? 'areasCreated',super._(); + const AreasCreated(final List areas, {final String? $type}): _areas = areas,$type = $type ?? 'areasCreated',super._(); factory AreasCreated.fromJson(Map json) => _$AreasCreatedFromJson(json); - final List _areas; - List get areas { + final List _areas; + List get areas { if (_areas is EqualUnmodifiableListView) return _areas; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(_areas); @@ -3303,7 +3303,7 @@ abstract mixin class $AreasCreatedCopyWith<$Res> implements $DocumentEventCopyWi factory $AreasCreatedCopyWith(AreasCreated value, $Res Function(AreasCreated) _then) = _$AreasCreatedCopyWithImpl; @useResult $Res call({ - List areas + List areas }); @@ -3323,7 +3323,7 @@ class _$AreasCreatedCopyWithImpl<$Res> @pragma('vm:prefer-inline') $Res call({Object? areas = null,}) { return _then(AreasCreated( null == areas ? _self._areas : areas // ignore: cast_nullable_to_non_nullable -as List, +as List, )); } diff --git a/api/lib/src/protocol/event.g.dart b/api/lib/src/protocol/event.g.dart index d33315a0302a..d57a4958e911 100644 --- a/api/lib/src/protocol/event.g.dart +++ b/api/lib/src/protocol/event.g.dart @@ -453,7 +453,7 @@ Map _$ElementsCollectionChangedToJson( AreasCreated _$AreasCreatedFromJson(Map json) => AreasCreated( (json['areas'] as List) - .map((e) => Area.fromJson(Map.from(e as Map))) + .map((e) => AreaPreset.fromJson(Map.from(e as Map))) .toList(), $type: json['type'] as String?, ); diff --git a/app/lib/bloc/document_bloc.dart b/app/lib/bloc/document_bloc.dart index 4cb303a7965e..21e4e92701d8 100644 --- a/app/lib/bloc/document_bloc.dart +++ b/app/lib/bloc/document_bloc.dart @@ -1167,41 +1167,65 @@ class DocumentBloc extends ReplayBloc { on((event, emit) async { final current = state; if (current is! DocumentLoadSuccess) return; - final areas = event.areas.map((e) { - var name = e.name; - var count = 1; - if (name.isEmpty) { - name = 'Area'; - } - while (current.page.areas.any((element) => element.name == name)) { - name = '${e.name} (${count++})'; - } - return e.copyWith(name: name); - }).toList(); + final areasByPage = groupBy( + event.areas, + (area) => area.page.isEmpty ? current.pageName : area.page, + ); + var data = current.data; + var activePage = current.page; var shouldRepaint = false; - for (var element in currentIndexCubit.renderers) { - final needRepaint = areas.any( - (area) => element.onAreaUpdate(current.data, current.page, area), - ); - if (needRepaint) { - shouldRepaint = true; + for (final entry in areasByPage.entries) { + final pageName = entry.key; + final page = pageName == current.pageName + ? activePage + : data.getPage(pageName); + if (page == null) continue; + final existingNames = page.areas.map((e) => e.name).toSet(); + final areas = entry.value + .map((preset) { + final area = preset.area; + if (area == null) return null; + final baseName = area.name.isEmpty ? 'Area' : area.name; + var name = baseName; + var count = 1; + while (existingNames.contains(name)) { + name = '$baseName (${count++})'; + } + existingNames.add(name); + return area.copyWith(name: name); + }) + .nonNulls + .toList(); + if (areas.isEmpty) continue; + if (pageName == current.pageName) { + for (var element in currentIndexCubit.renderers) { + final needRepaint = areas.any( + (area) => element.onAreaUpdate(data, page, area), + ); + if (needRepaint) { + shouldRepaint = true; + } + } } - } - final hasInitial = areas.any((e) => e.isInitial); - final existingAreas = current.page.areas.map((e) { - if (hasInitial && e.isInitial) { - return e.copyWith(isInitial: false); - } - return e; - }).toList(); + final hasInitial = areas.any((e) => e.isInitial); + final existingAreas = page.areas.map((e) { + if (hasInitial && e.isInitial) { + return e.copyWith(isInitial: false); + } + return e; + }).toList(); - final currentDocument = current.page.copyWith( - areas: [...existingAreas, ...areas], - ); + final updatedPage = page.copyWith(areas: [...existingAreas, ...areas]); + if (pageName == current.pageName) { + activePage = updatedPage; + } else { + data = data.setPage(updatedPage, pageName).$1; + } + } return _saveState( emit, - state: current.copyWith(page: currentDocument), + state: current.copyWith(data: data, page: activePage), shouldRefresh: () => true, reset: shouldRepaint, ); diff --git a/app/lib/cubits/current_index.dart b/app/lib/cubits/current_index.dart index f6ffcdd9d42a..711d7991a14f 100644 --- a/app/lib/cubits/current_index.dart +++ b/app/lib/cubits/current_index.dart @@ -2222,7 +2222,7 @@ class CurrentIndexCubit extends Cubit { name: name, ); final bloc = _activeDocumentBloc; - bloc?.add(AreasCreated([newArea])); + bloc?.add(AreasCreated([AreaPreset(area: newArea)])); bloc?.add(CurrentAreaChanged(name)); _teleportToAreaEdge(newArea, dx, dy); } diff --git a/app/lib/dialogs/template.dart b/app/lib/dialogs/template.dart index 5139702c6129..aa15967b4e29 100644 --- a/app/lib/dialogs/template.dart +++ b/app/lib/dialogs/template.dart @@ -782,6 +782,8 @@ class _TemplateDetailsViewState extends State<_TemplateDetailsView> { widget.template.getMetadata() ?? FileMetadata(type: NoteFileType.template); final info = widget.template.getInfo(); + final hasTemplateAreas = + widget.template.getPage()?.areas.isNotEmpty ?? false; final thumbnail = context.read().state.showThumbnails ? widget.template.getThumbnail() : null; @@ -901,7 +903,7 @@ class _TemplateDetailsViewState extends State<_TemplateDetailsView> { ), const SizedBox(height: 8), const Divider(), - buildAreaConfig(), + if (!hasTemplateAreas) buildAreaConfig(), if (widget.onOpen != null) Padding( padding: const EdgeInsets.only(top: 8.0), @@ -944,7 +946,7 @@ class _TemplateDetailsViewState extends State<_TemplateDetailsView> { ...details, const SizedBox(height: 16), const Divider(), - buildAreaConfig(), + if (!hasTemplateAreas) buildAreaConfig(), ], ), ), @@ -1295,6 +1297,7 @@ List _buildTemplateMenuChildren( final metadata = template.getMetadata()!; final templateBackgrounds = template.getPage()?.backgrounds ?? const []; + final templateAreas = template.getPage()?.areas ?? const []; return [ if (!isCore && fileSystem.storage == null) @@ -1359,6 +1362,24 @@ List _buildTemplateMenuChildren( _applyTemplateBackgroundsToPages(bloc, template, selectedPageNames); }, ), + if (bloc != null && templateAreas.isNotEmpty) + MenuItemButton( + leadingIcon: const PhosphorIcon(PhosphorIconsLight.selection), + child: Text(AppLocalizations.of(context).applyAreas), + onPressed: () async { + final state = bloc.state; + if (state is! DocumentLoadSuccess) return; + final selectedPageNames = await showDialog>( + context: context, + builder: (context) => SelectPagesDialog( + pages: state.data.getPagesWithNames(), + initialSelected: [state.pageName], + ), + ); + if (selectedPageNames == null) return; + _applyTemplateAreasToPages(bloc, template, selectedPageNames); + }, + ), MenuItemButton( leadingIcon: const PhosphorIcon(PhosphorIconsLight.copy), child: Text(AppLocalizations.of(context).duplicate), @@ -1454,3 +1475,19 @@ void _applyTemplateBackgroundsToPages( bloc.add(PageChanged(originalPageName)); } } + +void _applyTemplateAreasToPages( + DocumentBloc bloc, + NoteData template, + List pageNames, +) { + final state = bloc.state; + if (state is! DocumentLoadSuccess) return; + final areas = template.getPage()?.areas ?? const []; + bloc.add( + AreasCreated([ + for (final pageName in pageNames) + for (final area in areas) AreaPreset(page: pageName, area: area), + ]), + ); +} diff --git a/app/lib/handlers/area.dart b/app/lib/handlers/area.dart index 540fcf4d9e67..c7a737d93178 100644 --- a/app/lib/handlers/area.dart +++ b/app/lib/handlers/area.dart @@ -247,11 +247,13 @@ class AreaHandler extends Handler { currentRect = null; context.getDocumentBloc().add( AreasCreated([ - Area( - width: rect.width, - height: rect.height, - position: rect.topLeft.toPoint(), - name: name, + AreaPreset( + area: Area( + width: rect.width, + height: rect.height, + position: rect.topLeft.toPoint(), + name: name, + ), ), ]), ); diff --git a/app/lib/handlers/import.dart b/app/lib/handlers/import.dart index 0a493b74000f..c9610b5f743b 100644 --- a/app/lib/handlers/import.dart +++ b/app/lib/handlers/import.dart @@ -74,7 +74,11 @@ class ImportHandler extends Handler { context.addDocumentEvent( AreasCreated( data.areas - .map((e) => e.copyWith(position: e.position + _offset.toPoint())) + .map( + (e) => AreaPreset( + area: e.copyWith(position: e.position + _offset.toPoint()), + ), + ) .toList(), ), ); diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 2d7548bd558f..02ed11cb0a87 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -99,6 +99,7 @@ }, "background": "Background", "applyBackground": "Apply background", + "applyAreas": "Apply areas", "currentPage": "Current page", "allPages": "All pages", "box": "Box", diff --git a/app/lib/services/import.dart b/app/lib/services/import.dart index fa27315a06ff..86bdef20717d 100644 --- a/app/lib/services/import.dart +++ b/app/lib/services/import.dart @@ -170,7 +170,7 @@ class ImportResult { bloc?.add(AssetUpdated(path, data)); } bloc - ?..add(AreasCreated(areas)) + ?..add(AreasCreated(areas.map((e) => AreaPreset(area: e)).toList())) ..add(ElementsCreated(elements, assets: _importAssets)); } bloc?.add( diff --git a/app/lib/views/navigator/areas.dart b/app/lib/views/navigator/areas.dart index 1064337c178d..e2fa75175624 100644 --- a/app/lib/views/navigator/areas.dart +++ b/app/lib/views/navigator/areas.dart @@ -85,12 +85,14 @@ class _AreasViewState extends State { bloc ..add( AreasCreated([ - Area( - name: name, - width: width, - height: height, - position: position.toPoint(), - isInitial: config.areaAsInitial, + AreaPreset( + area: Area( + name: name, + width: width, + height: height, + position: position.toPoint(), + isInitial: config.areaAsInitial, + ), ), ]), ) From 3599d83af46cae99a21cf43434b6c2e35e1653e5 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 28 Jun 2026 18:22:13 +0200 Subject: [PATCH 022/117] Update changelog and dependencies --- SECURITY.md | 32 +++++++++++----------- app/android/Gemfile.lock | 20 +++++++------- app/pubspec.lock | 12 ++++----- docs/pnpm-lock.yaml | 44 +++++++++++++++---------------- metadata/en-US/changelogs/187.txt | 4 +++ tools/pubspec.lock | 6 ++--- 6 files changed, 61 insertions(+), 57 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index b419197544d5..1daa655498a3 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,22 +2,22 @@ ## Supported Versions -| Version | Supported | | -| ------------------------ | ------------------ | --------------------------------------------------------------------------- | -| 2.5-dev (Crimson Red) | :warning: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.5.3-rc.1) | -| 2.5.3 (Crimson Red) | :white_check_mark: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.5.3) | -| 2.4.4 (Black Hairstreak) | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.4.4) | -| 2.3.4 (Adonis Blue) | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.3.4) | -| 2.2.4 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.2.4) | -| 2.1.1 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.1.1) | -| 2.0.3 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.0.3) | -| 1.6.1 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v1.6.1) | -| 1.5.1 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v1.5.1) | -| 1.4.4 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v1.4.4) | -| 1.3.2 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v1.3.2) | -| 1.2.1 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v1.2.1) | -| 1.1.2 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v1.1.2) | -| 1.0.0 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v1.0.0) | +| Version | Supported | | +| -------------------------- | ------------------ | ----------------------------------------------------------------------------- | +| 2.6-dev (Dreamy Duskywing) | :warning: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.6.0-beta.0) | +| 2.5.3 (Crimson Red) | :white_check_mark: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.5.3) | +| 2.4.4 (Black Hairstreak) | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.4.4) | +| 2.3.4 (Adonis Blue) | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.3.4) | +| 2.2.4 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.2.4) | +| 2.1.1 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.1.1) | +| 2.0.3 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.0.3) | +| 1.6.1 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v1.6.1) | +| 1.5.1 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v1.5.1) | +| 1.4.4 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v1.4.4) | +| 1.3.2 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v1.3.2) | +| 1.2.1 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v1.2.1) | +| 1.1.2 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v1.1.2) | +| 1.0.0 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v1.0.0) | Older versions can be found [here](https://butterfly.linwood.dev/community/pre-1-0). diff --git a/app/android/Gemfile.lock b/app/android/Gemfile.lock index 5f5d6f989971..f3cafe246c4d 100644 --- a/app/android/Gemfile.lock +++ b/app/android/Gemfile.lock @@ -8,7 +8,7 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1260.0) + aws-partitions (1.1262.0) aws-sdk-core (3.252.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -20,7 +20,7 @@ GEM aws-sdk-kms (1.129.0) aws-sdk-core (~> 3, >= 3.248.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.225.1) + aws-sdk-s3 (1.226.0) aws-sdk-core (~> 3, >= 3.248.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -43,7 +43,7 @@ GEM dotenv (2.8.1) emoji_regex (3.2.3) excon (0.112.0) - faraday (1.10.5) + faraday (1.10.6) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -125,7 +125,7 @@ GEM xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) fastlane-sirp (1.1.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.102.0) + google-apis-androidpublisher_v3 (0.104.0) google-apis-core (>= 0.15.0, < 2.a) google-apis-core (0.18.0) addressable (~> 2.5, >= 2.5.1) @@ -135,11 +135,11 @@ GEM mutex_m representable (~> 3.0) retriable (>= 2.0, < 4.a) - google-apis-iamcredentials_v1 (0.27.0) + google-apis-iamcredentials_v1 (0.28.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-playcustomapp_v1 (0.17.0) + google-apis-playcustomapp_v1 (0.18.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.63.0) + google-apis-storage_v1 (0.64.0) google-apis-core (>= 0.15.0, < 2.a) google-cloud-core (1.9.0) google-cloud-env (>= 1.0, < 3.a) @@ -158,7 +158,7 @@ GEM googleauth (~> 1.9) mini_mime (~> 1.0) google-logging-utils (0.2.0) - googleauth (1.17.0) + googleauth (1.17.1) faraday (>= 1.0, < 3.a) google-cloud-env (~> 2.2) google-logging-utils (~> 0.1) @@ -172,7 +172,7 @@ GEM httpclient (2.9.0) mutex_m jmespath (1.6.2) - json (2.19.9) + json (2.20.0) jwt (3.2.0) base64 logger (1.7.0) @@ -244,4 +244,4 @@ DEPENDENCIES screengrab BUNDLED WITH - 4.0.14 + 4.0.15 diff --git a/app/pubspec.lock b/app/pubspec.lock index 9a5bbb8786ff..f311ee738840 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -667,10 +667,10 @@ packages: dependency: "direct main" description: name: idb_shim - sha256: "65d2fbda05f707f9c6e0b502bc4bf411d9f799d391f709c121aeb44d0dc0fd2a" + sha256: "2c81d22578b71951004f85ad3cf0eca23aeea1e18b74cf5efce2a267dce17579" url: "https://pub.dev" source: hosted - version: "2.9.4" + version: "2.9.5" image: dependency: "direct main" description: @@ -869,10 +869,10 @@ packages: dependency: "direct dev" description: name: msix - sha256: bed33b53662c7eac66dc4f9b4176d03140307e6a892ab21ce529be6f74393db0 + sha256: "61415c352e8aea084332b8a5514e6408c961c8cdeca202804fff1040eb555ac7" url: "https://pub.dev" source: hosted - version: "3.17.0" + version: "3.18.0" native_toolchain_c: dependency: transitive description: @@ -1274,10 +1274,10 @@ packages: dependency: transitive description: name: sembast - sha256: "60746eb3377fe953367c10d549e9d99a3e0bccb464f32d0528d4e44933898597" + sha256: a58b26925e23071cf0f4754d8449aabe829e9a9930a872fb18e6ae4c2c88e025 url: "https://pub.dev" source: hosted - version: "3.8.9" + version: "3.8.9+1" share_plus: dependency: "direct main" description: diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index df201d902897..a42a09e02baa 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@astrojs/check': specifier: ^0.9.9 - version: 0.9.9(prettier@3.8.5)(typescript@6.0.3) + version: 0.9.9(prettier@3.9.0)(typescript@6.0.3) '@astrojs/markdown-satteri': specifier: ^0.3.2 version: 0.3.2 @@ -2136,8 +2136,8 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.5.379: - resolution: {integrity: sha512-v/qV5aV5EUA2pGilzUCq5/eyOloZAqDZBu9UMBIzgPpLlprjSR6zswsWBTv0KpqxLGUAZEwhO95ZCt7srymNVA==} + electron-to-chromium@1.5.380: + resolution: {integrity: sha512-W6d5AbuEoRayO447cqrg6lKJIlscgRnnxOZl/08kfV71BQDoEBC7Wwis68z87LjyK6f4kWyTaubuDbhHKrZkbA==} emmet@2.4.11: resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} @@ -2653,8 +2653,8 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.2.0: - resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true jsesc@3.1.0: @@ -3134,8 +3134,8 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} - prettier@3.8.5: - resolution: {integrity: sha512-zxcTTCedNGJM4R8sj/Cq/F0W/c4iE0afWBcBwMTRtw4WHYP9TWkYjdiH3npPRUYsXQCPR0hTU9yjovOu+E6EQA==} + prettier@3.9.0: + resolution: {integrity: sha512-LjIqSIC5VYLzs9WedVmJ2ljNAGnU+DteIClbahu4L/DBeWjZ6iT/k1lAYyu9JUh+1xINxWadaPw/Pl63y/agAw==} engines: {node: '>=14'} hasBin: true @@ -4052,9 +4052,9 @@ snapshots: jsonpointer: 5.0.1 leven: 3.1.0 - '@astrojs/check@0.9.9(prettier@3.8.5)(typescript@6.0.3)': + '@astrojs/check@0.9.9(prettier@3.9.0)(typescript@6.0.3)': dependencies: - '@astrojs/language-server': 2.16.10(prettier@3.8.5)(typescript@6.0.3) + '@astrojs/language-server': 2.16.10(prettier@3.9.0)(typescript@6.0.3) chokidar: 4.0.3 kleur: 4.1.5 typescript: 6.0.3 @@ -4123,14 +4123,14 @@ snapshots: dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - js-yaml: 4.2.0 + js-yaml: 4.3.0 picomatch: 4.0.4 retext-smartypants: 6.2.0 shiki: 4.3.0 smol-toml: 1.7.0 unified: 11.0.5 - '@astrojs/language-server@2.16.10(prettier@3.8.5)(typescript@6.0.3)': + '@astrojs/language-server@2.16.10(prettier@3.9.0)(typescript@6.0.3)': dependencies: '@astrojs/compiler': 2.13.1 '@astrojs/yaml2ts': 0.2.4 @@ -4144,14 +4144,14 @@ snapshots: volar-service-css: 0.0.70(@volar/language-service@2.4.28) volar-service-emmet: 0.0.70(@volar/language-service@2.4.28) volar-service-html: 0.0.70(@volar/language-service@2.4.28) - volar-service-prettier: 0.0.70(@volar/language-service@2.4.28)(prettier@3.8.5) + volar-service-prettier: 0.0.70(@volar/language-service@2.4.28)(prettier@3.9.0) volar-service-typescript: 0.0.70(@volar/language-service@2.4.28) volar-service-typescript-twoslash-queries: 0.0.70(@volar/language-service@2.4.28) volar-service-yaml: 0.0.70(@volar/language-service@2.4.28) vscode-html-languageservice: 5.6.2 vscode-uri: 3.1.0 optionalDependencies: - prettier: 3.8.5 + prettier: 3.9.0 transitivePeerDependencies: - typescript @@ -4259,7 +4259,7 @@ snapshots: hast-util-to-string: 3.0.1 hastscript: 9.0.1 i18next: 26.3.3(typescript@6.0.3) - js-yaml: 4.2.0 + js-yaml: 4.3.0 klona: 2.0.6 magic-string: 0.30.21 mdast-util-directive: 3.1.0 @@ -5902,7 +5902,7 @@ snapshots: github-slugger: 2.0.0 html-escaper: 3.0.3 http-cache-semantics: 4.2.0 - js-yaml: 4.2.0 + js-yaml: 4.3.0 jsonc-parser: 3.3.1 magic-string: 0.30.21 magicast: 0.5.3 @@ -6035,7 +6035,7 @@ snapshots: dependencies: baseline-browser-mapping: 2.10.40 caniuse-lite: 1.0.30001799 - electron-to-chromium: 1.5.379 + electron-to-chromium: 1.5.380 node-releases: 2.0.50 update-browserslist-db: 1.2.3(browserslist@4.28.4) @@ -6244,7 +6244,7 @@ snapshots: dependencies: jake: 10.9.4 - electron-to-chromium@1.5.379: {} + electron-to-chromium@1.5.380: {} emmet@2.4.11: dependencies: @@ -6977,7 +6977,7 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.2.0: + js-yaml@4.3.0: dependencies: argparse: 2.0.1 @@ -7709,7 +7709,7 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prettier@3.8.5: {} + prettier@3.9.0: {} pretty-bytes@5.6.0: {} @@ -8561,12 +8561,12 @@ snapshots: optionalDependencies: '@volar/language-service': 2.4.28 - volar-service-prettier@0.0.70(@volar/language-service@2.4.28)(prettier@3.8.5): + volar-service-prettier@0.0.70(@volar/language-service@2.4.28)(prettier@3.9.0): dependencies: vscode-uri: 3.1.0 optionalDependencies: '@volar/language-service': 2.4.28 - prettier: 3.8.5 + prettier: 3.9.0 volar-service-typescript-twoslash-queries@0.0.70(@volar/language-service@2.4.28): dependencies: @@ -8834,7 +8834,7 @@ snapshots: '@vscode/l10n': 0.0.18 ajv: 8.20.0 ajv-draft-04: 1.0.0(ajv@8.20.0) - prettier: 3.8.5 + prettier: 3.9.0 request-light: 0.5.8 vscode-json-languageservice: 4.1.8 vscode-languageserver: 9.0.1 diff --git a/metadata/en-US/changelogs/187.txt b/metadata/en-US/changelogs/187.txt index c6b7ae570476..21ae4f3f7aef 100644 --- a/metadata/en-US/changelogs/187.txt +++ b/metadata/en-US/changelogs/187.txt @@ -1,3 +1,7 @@ +* Add pages selector with range input ([#1151](https://github.com/LinwoodDev/Butterfly/issues/1151)) +* Add internal page numbers to the pages navigator ([#1143](https://github.com/LinwoodDev/Butterfly/issues/1143)) +* Add cross-page area selection and deletion ([#1143](https://github.com/LinwoodDev/Butterfly/issues/1143)) +* Add apply areas option to templates ([#1151](https://github.com/LinwoodDev/Butterfly/issues/1151)) * Add combine paths option ([#1071](https://github.com/LinwoodDev/Butterfly/issues/1071)) * Add xournal++ exporter * Add next and previous page shortcuts diff --git a/tools/pubspec.lock b/tools/pubspec.lock index 0a53fd4b772f..4934bb9ef0cb 100644 --- a/tools/pubspec.lock +++ b/tools/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.20.3" lints: dependency: "direct main" description: @@ -130,4 +130,4 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.0 <4.0.0" + dart: ">=3.9.0 <4.0.0" From 41be77ea50b27dfe89f0dbc6d730aea68feef697 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 30 Jun 2026 19:23:34 +0200 Subject: [PATCH 023/117] Improve window title bar design --- app/pubspec.lock | 40 ++++++++----------- app/pubspec.yaml | 4 +- docs/pnpm-lock.yaml | 66 +++++++++++++++---------------- metadata/en-US/changelogs/187.txt | 1 + 4 files changed, 52 insertions(+), 59 deletions(-) diff --git a/app/pubspec.lock b/app/pubspec.lock index f311ee738840..089117761427 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -236,10 +236,10 @@ packages: dependency: transitive description: name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.2.1" collection: dependency: "direct main" description: @@ -484,18 +484,18 @@ packages: dependency: transitive description: name: flutter_rust_bridge - sha256: "37bcf055414b4b6417a046d536c16d09a1f58bdf099f45a91008f0fe49f641c0" + sha256: "20861a4148ebb72f547366b2dd575ef803e32ff1cb6dc5aa0c9ffdbc774f2799" url: "https://pub.dev" source: hosted - version: "2.13.0-beta.2" + version: "2.13.0-beta.4" flutter_rust_bridge_hooks: dependency: transitive description: name: flutter_rust_bridge_hooks - sha256: bebf66b09522335b99a77728fd9856ac76bd40e66431db21f01307a54b78f0f9 + sha256: "0ae752d541b6878ac1baac7827502ea21cb3ac81bf57362895a6df2929707ac7" url: "https://pub.dev" source: hosted - version: "2.13.0-beta.2" + version: "2.13.0-beta.4" flutter_secure_storage: dependency: "direct main" description: @@ -627,10 +627,10 @@ packages: dependency: transitive description: name: hooks - sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "2.0.2" html: dependency: "direct main" description: @@ -836,8 +836,8 @@ packages: dependency: "direct main" description: path: "packages/material_leap" - ref: "064aed61c361194bf0eb3cb599e62bdd3c111d7c" - resolved-ref: "064aed61c361194bf0eb3cb599e62bdd3c111d7c" + ref: bc69ad6ef16c6730ba52dfd466ae7d0c0af2e082 + resolved-ref: bc69ad6ef16c6730ba52dfd466ae7d0c0af2e082 url: "https://github.com/LinwoodDev/dart_pkgs.git" source: git version: "0.0.1" @@ -873,22 +873,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.18.0" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" - url: "https://pub.dev" - source: hosted - version: "0.17.6" native_toolchain_rust: dependency: transitive description: name: native_toolchain_rust - sha256: "26d4dcae954328af4ebfa8fc56cca0bf74b9ae8b4a6132de5da2071ac3d237b3" + sha256: faa57d2258a3b0fd2a634054f54e4496c9fcbd971977e7d2b7e6916d56892857 url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.4+0" nested: dependency: transitive description: @@ -952,10 +944,10 @@ packages: dependency: transitive description: name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.4.1" one_dollar_unistroke_recognizer: dependency: "direct main" description: @@ -968,8 +960,8 @@ packages: dependency: "direct main" description: path: "packages/onenote_parser" - ref: "5d2919c34bcd128bc904a1cfe18b03cb37f02006" - resolved-ref: "5d2919c34bcd128bc904a1cfe18b03cb37f02006" + ref: d7d00d81291d6a2716da40b9f1d004ac4973771d + resolved-ref: d7d00d81291d6a2716da40b9f1d004ac4973771d url: "https://github.com/LinwoodDev/dart_pkgs.git" source: git version: "0.0.1" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 8954403ef774..707aa8f5da83 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -62,7 +62,7 @@ dependencies: material_leap: git: url: https://github.com/LinwoodDev/dart_pkgs.git - ref: 064aed61c361194bf0eb3cb599e62bdd3c111d7c + ref: bc69ad6ef16c6730ba52dfd466ae7d0c0af2e082 path: packages/material_leap lw_sysapi: git: @@ -109,7 +109,7 @@ dependencies: onenote_parser: git: url: https://github.com/LinwoodDev/dart_pkgs.git - ref: 5d2919c34bcd128bc904a1cfe18b03cb37f02006 + ref: d7d00d81291d6a2716da40b9f1d004ac4973771d path: packages/onenote_parser web: ^1.0.0 cryptography_plus: ^3.0.0 diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index a42a09e02baa..78e9dd705cd0 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@astrojs/check': specifier: ^0.9.9 - version: 0.9.9(prettier@3.9.0)(typescript@6.0.3) + version: 0.9.9(prettier@3.9.3)(typescript@6.0.3) '@astrojs/markdown-satteri': specifier: ^0.3.2 version: 0.3.2 @@ -2136,8 +2136,8 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.5.380: - resolution: {integrity: sha512-W6d5AbuEoRayO447cqrg6lKJIlscgRnnxOZl/08kfV71BQDoEBC7Wwis68z87LjyK6f4kWyTaubuDbhHKrZkbA==} + electron-to-chromium@1.5.381: + resolution: {integrity: sha512-n9Wa6yB+vDsGuA8AKbl/0z7HbvWqt5jxIdvr1IUicd0ryPrk7/xzwqLv8D9AbbvZ6avVNtXYLTfmgFHkwkyelg==} emmet@2.4.11: resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} @@ -2169,8 +2169,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@2.1.0: - resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-module-lexer@2.2.0: + resolution: {integrity: sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ==} es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} @@ -2255,8 +2255,8 @@ packages: fast-string-width@3.0.2: resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - fast-uri@3.1.2: - resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fast-uri@3.1.3: + resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} @@ -3130,12 +3130,12 @@ packages: resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==} engines: {node: '>=4'} - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} - prettier@3.9.0: - resolution: {integrity: sha512-LjIqSIC5VYLzs9WedVmJ2ljNAGnU+DteIClbahu4L/DBeWjZ6iT/k1lAYyu9JUh+1xINxWadaPw/Pl63y/agAw==} + prettier@3.9.3: + resolution: {integrity: sha512-HWmu+K+zvHNpaMfSnYeqdqrDbR16cuIXaPx8WoHaviQkDJh1/0BNtOZmHVQI5jc3wXv0H1yXc9wjvFdXh+n3hQ==} engines: {node: '>=14'} hasBin: true @@ -4052,9 +4052,9 @@ snapshots: jsonpointer: 5.0.1 leven: 3.1.0 - '@astrojs/check@0.9.9(prettier@3.9.0)(typescript@6.0.3)': + '@astrojs/check@0.9.9(prettier@3.9.3)(typescript@6.0.3)': dependencies: - '@astrojs/language-server': 2.16.10(prettier@3.9.0)(typescript@6.0.3) + '@astrojs/language-server': 2.16.10(prettier@3.9.3)(typescript@6.0.3) chokidar: 4.0.3 kleur: 4.1.5 typescript: 6.0.3 @@ -4130,7 +4130,7 @@ snapshots: smol-toml: 1.7.0 unified: 11.0.5 - '@astrojs/language-server@2.16.10(prettier@3.9.0)(typescript@6.0.3)': + '@astrojs/language-server@2.16.10(prettier@3.9.3)(typescript@6.0.3)': dependencies: '@astrojs/compiler': 2.13.1 '@astrojs/yaml2ts': 0.2.4 @@ -4144,14 +4144,14 @@ snapshots: volar-service-css: 0.0.70(@volar/language-service@2.4.28) volar-service-emmet: 0.0.70(@volar/language-service@2.4.28) volar-service-html: 0.0.70(@volar/language-service@2.4.28) - volar-service-prettier: 0.0.70(@volar/language-service@2.4.28)(prettier@3.9.0) + volar-service-prettier: 0.0.70(@volar/language-service@2.4.28)(prettier@3.9.3) volar-service-typescript: 0.0.70(@volar/language-service@2.4.28) volar-service-typescript-twoslash-queries: 0.0.70(@volar/language-service@2.4.28) volar-service-yaml: 0.0.70(@volar/language-service@2.4.28) vscode-html-languageservice: 5.6.2 vscode-uri: 3.1.0 optionalDependencies: - prettier: 3.9.0 + prettier: 3.9.3 transitivePeerDependencies: - typescript @@ -4191,7 +4191,7 @@ snapshots: '@mdx-js/mdx': 3.1.1 acorn: 8.17.0 astro: 7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - es-module-lexer: 2.1.0 + es-module-lexer: 2.2.0 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 piccolore: 0.1.3 @@ -5138,8 +5138,8 @@ snapshots: hast-util-to-html: 9.0.5 hast-util-to-text: 4.0.2 hastscript: 9.0.1 - postcss: 8.5.15 - postcss-nested: 6.2.0(postcss@8.5.15) + postcss: 8.5.16 + postcss-nested: 6.2.0(postcss@8.5.16) unist-util-visit: 5.1.0 unist-util-visit-parents: 6.0.2 @@ -5824,7 +5824,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 + fast-uri: 3.1.3 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -5894,7 +5894,7 @@ snapshots: devalue: 5.8.1 diff: 8.0.4 dset: 3.1.4 - es-module-lexer: 2.1.0 + es-module-lexer: 2.2.0 esbuild: 0.28.1 flattie: 1.1.1 fontace: 0.4.1 @@ -6035,7 +6035,7 @@ snapshots: dependencies: baseline-browser-mapping: 2.10.40 caniuse-lite: 1.0.30001799 - electron-to-chromium: 1.5.380 + electron-to-chromium: 1.5.381 node-releases: 2.0.50 update-browserslist-db: 1.2.3(browserslist@4.28.4) @@ -6244,7 +6244,7 @@ snapshots: dependencies: jake: 10.9.4 - electron-to-chromium@1.5.380: {} + electron-to-chromium@1.5.381: {} emmet@2.4.11: dependencies: @@ -6325,7 +6325,7 @@ snapshots: es-errors@1.3.0: {} - es-module-lexer@2.1.0: {} + es-module-lexer@2.2.0: {} es-object-atoms@1.1.2: dependencies: @@ -6454,7 +6454,7 @@ snapshots: dependencies: fast-string-truncated-width: 3.0.3 - fast-uri@3.1.2: {} + fast-uri@3.1.3: {} fast-wrap-ansi@0.2.2: dependencies: @@ -7693,9 +7693,9 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-nested@6.2.0(postcss@8.5.15): + postcss-nested@6.2.0(postcss@8.5.16): dependencies: - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 6.1.4 postcss-selector-parser@6.1.4: @@ -7703,13 +7703,13 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss@8.5.15: + postcss@8.5.16: dependencies: nanoid: 3.3.15 picocolors: 1.1.1 source-map-js: 1.2.1 - prettier@3.9.0: {} + prettier@3.9.3: {} pretty-bytes@5.6.0: {} @@ -8521,7 +8521,7 @@ snapshots: dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.15 + postcss: 8.5.16 rolldown: 1.1.3 tinyglobby: 0.2.17 optionalDependencies: @@ -8561,12 +8561,12 @@ snapshots: optionalDependencies: '@volar/language-service': 2.4.28 - volar-service-prettier@0.0.70(@volar/language-service@2.4.28)(prettier@3.9.0): + volar-service-prettier@0.0.70(@volar/language-service@2.4.28)(prettier@3.9.3): dependencies: vscode-uri: 3.1.0 optionalDependencies: '@volar/language-service': 2.4.28 - prettier: 3.9.0 + prettier: 3.9.3 volar-service-typescript-twoslash-queries@0.0.70(@volar/language-service@2.4.28): dependencies: @@ -8834,7 +8834,7 @@ snapshots: '@vscode/l10n': 0.0.18 ajv: 8.20.0 ajv-draft-04: 1.0.0(ajv@8.20.0) - prettier: 3.9.0 + prettier: 3.9.3 request-light: 0.5.8 vscode-json-languageservice: 4.1.8 vscode-languageserver: 9.0.1 diff --git a/metadata/en-US/changelogs/187.txt b/metadata/en-US/changelogs/187.txt index 21ae4f3f7aef..449930c80146 100644 --- a/metadata/en-US/changelogs/187.txt +++ b/metadata/en-US/changelogs/187.txt @@ -7,6 +7,7 @@ * Add next and previous page shortcuts * Improve xournal++ importer * Improve state management for better linking different systems together +* Improve window title bar design * Unify area context menu and area selection context menu ([#1151](https://github.com/LinwoodDev/Butterfly/issues/1151)) * Fix refresh foregrounds can be run concurrently * Fix location synchronization issues From 764a7ae6dbb513870e66316b335b574d4fcfaa88 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 30 Jun 2026 22:47:17 +0200 Subject: [PATCH 024/117] Fix zoom slider and reset button not working if zoom is locked --- app/lib/views/zoom.dart | 2 +- metadata/en-US/changelogs/187.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/lib/views/zoom.dart b/app/lib/views/zoom.dart index 47820adc028e..539dc30ea6e5 100644 --- a/app/lib/views/zoom.dart +++ b/app/lib/views/zoom.dart @@ -73,7 +73,7 @@ class _ZoomViewState extends State with TickerProviderStateMixin { } final size = currentIndex.cameraViewport.toRealSize(); final center = Offset(size.width / 2, size.height / 2); - currentIndexCubit.size(value, center); + currentIndexCubit.size(value, center, true); if (bake) { currentIndexCubit.bake(documentState); } diff --git a/metadata/en-US/changelogs/187.txt b/metadata/en-US/changelogs/187.txt index 449930c80146..91b9c9e78395 100644 --- a/metadata/en-US/changelogs/187.txt +++ b/metadata/en-US/changelogs/187.txt @@ -11,5 +11,6 @@ * Unify area context menu and area selection context menu ([#1151](https://github.com/LinwoodDev/Butterfly/issues/1151)) * Fix refresh foregrounds can be run concurrently * Fix location synchronization issues +* Fix zoom slider and reset button not working if zoom is locked Read more here: https://linwood.dev/butterfly/2.6.0-beta.1 \ No newline at end of file From 709c11a383e466c9eb44f30c0dfa5cdceadbcd1d Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Wed, 1 Jul 2026 00:09:50 +0200 Subject: [PATCH 025/117] Fix saved documents being saved again --- app/lib/cubits/current_index.dart | 7 +++- app/lib/views/main.dart | 5 +++ app/test/bloc/document_bloc_test.dart | 9 ++++++ .../views/project_page_lifecycle_test.dart | 32 +++++++++++++++++++ metadata/en-US/changelogs/187.txt | 2 ++ 5 files changed, 54 insertions(+), 1 deletion(-) diff --git a/app/lib/cubits/current_index.dart b/app/lib/cubits/current_index.dart index 711d7991a14f..52c1ca92fabb 100644 --- a/app/lib/cubits/current_index.dart +++ b/app/lib/cubits/current_index.dart @@ -2414,7 +2414,7 @@ class CurrentIndexCubit extends Cubit { bool isAutosave = false, }) async { final absolute = state.absolute; - if (!force && + if (location == null && (state.saved == SaveState.saved || state.saved == SaveState.absoluteRead)) { return state.location; @@ -2437,6 +2437,11 @@ class CurrentIndexCubit extends Cubit { } } return _savingLock.synchronized(() async { + if (location == null && + (state.saved == SaveState.saved || + state.saved == SaveState.absoluteRead)) { + return state.location; + } var current = location ?? state.location; if (isClosed) { return current; diff --git a/app/lib/views/main.dart b/app/lib/views/main.dart index b83fdf800681..148187981b02 100644 --- a/app/lib/views/main.dart +++ b/app/lib/views/main.dart @@ -385,6 +385,11 @@ class _ProjectPageState extends State { page, pageName, ); + final isImportedDocument = + documentOpened && !(location.fileType?.isNote() ?? false); + if (!absolute && isImportedDocument) { + currentIndexCubit.setSaveState(saved: SaveState.unsaved); + } networkingService.setup(bloc); setState(() { _runtime = _ProjectDocumentRuntime( diff --git a/app/test/bloc/document_bloc_test.dart b/app/test/bloc/document_bloc_test.dart index 7147df031560..96649aec6fbc 100644 --- a/app/test/bloc/document_bloc_test.dart +++ b/app/test/bloc/document_bloc_test.dart @@ -262,6 +262,15 @@ void main() { }, ); + test('force saving an already saved document does not write again', () async { + expect(currentIndexCubit.state.saved, SaveState.saved); + + final location = await currentIndexCubit.save(bloc, force: true); + + expect(location, const AssetLocation(path: 'test-note.bfly')); + expect(currentIndexCubit.state.saved, SaveState.saved); + }); + test('duplicating area adds it to selected pages', () async { final initialState = bloc.state as DocumentLoadSuccess; final pages = initialState.data.getPages(true); diff --git a/app/test/views/project_page_lifecycle_test.dart b/app/test/views/project_page_lifecycle_test.dart index 420cabc39344..1183571b98b8 100644 --- a/app/test/views/project_page_lifecycle_test.dart +++ b/app/test/views/project_page_lifecycle_test.dart @@ -133,6 +133,10 @@ void main() { ); }, ), + GoRoute( + path: 'import', + builder: (context, state) => ProjectPage(data: document), + ), ], ), ], @@ -196,6 +200,32 @@ void main() { expect(observer.currentIndexCubitCreates, 3); expect(observer.currentIndexCubitCloses, 3); }); + + testWidgets('converted imported file starts unsaved', (tester) async { + await tester.pumpWidget(buildApp()); + + router.go('/import'); + await pumpUntil( + tester, + () => + find.byType(ProjectPage).evaluate().isNotEmpty && + observer.lastCurrentIndexCubit != null, + 'imported document open', + ); + + expect(observer.lastCurrentIndexCubit!.state.saved, SaveState.unsaved); + + router.go('/'); + await pumpUntil( + tester, + () => + find.byType(ProjectPage).evaluate().isEmpty && + find.byType(ProjectPage, skipOffstage: false).evaluate().isEmpty && + observer.documentBlocCloses == 1 && + observer.currentIndexCubitCloses == 1, + 'imported document close', + ); + }); } class _LifecycleObserver extends BlocObserver { @@ -203,6 +233,7 @@ class _LifecycleObserver extends BlocObserver { int documentBlocCloses = 0; int currentIndexCubitCreates = 0; int currentIndexCubitCloses = 0; + CurrentIndexCubit? lastCurrentIndexCubit; final events = []; @override @@ -212,6 +243,7 @@ class _LifecycleObserver extends BlocObserver { documentBlocCreates++; } else if (bloc is CurrentIndexCubit) { currentIndexCubitCreates++; + lastCurrentIndexCubit = bloc; } } diff --git a/metadata/en-US/changelogs/187.txt b/metadata/en-US/changelogs/187.txt index 91b9c9e78395..30157d550877 100644 --- a/metadata/en-US/changelogs/187.txt +++ b/metadata/en-US/changelogs/187.txt @@ -11,6 +11,8 @@ * Unify area context menu and area selection context menu ([#1151](https://github.com/LinwoodDev/Butterfly/issues/1151)) * Fix refresh foregrounds can be run concurrently * Fix location synchronization issues +* Fix saved documents being saved again +* Fix imported documents starting as saved * Fix zoom slider and reset button not working if zoom is locked Read more here: https://linwood.dev/butterfly/2.6.0-beta.1 \ No newline at end of file From 2adb49ff81cba81bc42e4ad48a1c4e76f03777a6 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Wed, 1 Jul 2026 00:44:28 +0200 Subject: [PATCH 026/117] Disable the save button if it is already saved --- app/lib/views/app_bar.dart | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/lib/views/app_bar.dart b/app/lib/views/app_bar.dart index 3cb0f2fd833b..27159eab9910 100644 --- a/app/lib/views/app_bar.dart +++ b/app/lib/views/app_bar.dart @@ -419,12 +419,20 @@ class _AppBarTitleState extends State<_AppBarTitle> { context, ).readOnly, }; + final canSave = + currentIndex.saved == SaveState.unsaved || + currentIndex.isSaveDelayed; return IconButton( icon: icon, tooltip: tooltip, - onPressed: () { - Actions.maybeInvoke(context, SaveIntent()); - }, + onPressed: canSave + ? () { + Actions.maybeInvoke( + context, + SaveIntent(), + ); + } + : null, ); }, ), From 0aba14804139120369c102d1cf39d3c32a715b84 Mon Sep 17 00:00:00 2001 From: CodeDoctor Date: Sun, 5 Jul 2026 17:42:59 +0200 Subject: [PATCH 027/117] New Crowdin updates (#1149) * New translations app_en.arb (Czech) [ci skip] [ci skip] * New translations app_en.arb (Danish) [ci skip] [ci skip] * New translations app_en.arb (German) [ci skip] [ci skip] * New translations app_en.arb (Greek) [ci skip] [ci skip] * New translations app_en.arb (Finnish) [ci skip] [ci skip] * New translations app_en.arb (Italian) [ci skip] [ci skip] * New translations app_en.arb (Japanese) [ci skip] [ci skip] * New translations app_en.arb (Dutch) [ci skip] [ci skip] * New translations app_en.arb (Norwegian) [ci skip] [ci skip] * New translations app_en.arb (Polish) [ci skip] [ci skip] * New translations app_en.arb (Portuguese) [ci skip] [ci skip] * New translations app_en.arb (Russian) [ci skip] [ci skip] * New translations app_en.arb (Swedish) [ci skip] [ci skip] * New translations app_en.arb (Ukrainian) [ci skip] [ci skip] * New translations app_en.arb (Chinese Simplified) [ci skip] [ci skip] * New translations app_en.arb (Portuguese, Brazilian) [ci skip] [ci skip] * New translations app_en.arb (Indonesian) [ci skip] [ci skip] * New translations app_en.arb (Afrikaans) [ci skip] [ci skip] * New translations app_en.arb (Catalan) [ci skip] [ci skip] * New translations app_en.arb (Hebrew) [ci skip] [ci skip] * New translations app_en.arb (Hungarian) [ci skip] [ci skip] * New translations app_en.arb (Korean) [ci skip] [ci skip] * New translations app_en.arb (Turkish) [ci skip] [ci skip] * New translations app_en.arb (Chinese Traditional) [ci skip] [ci skip] * New translations app_en.arb (Vietnamese) [ci skip] [ci skip] * New translations app_en.arb (Thai) [ci skip] [ci skip] * New translations app_en.arb (Hindi) [ci skip] [ci skip] * New translations app_en.arb (Odia) [ci skip] [ci skip] * New translations app_en.arb (Serbian) [ci skip] [ci skip] * New translations faq.md (Romanian) [ci skip] [ci skip] * New translations label.md (Romanian) [ci skip] [ci skip] * New translations stylus-support.md (Romanian) [ci skip] [ci skip] * New translations faq.md (French) [ci skip] [ci skip] * New translations label.md (French) [ci skip] [ci skip] * New translations stylus-support.md (French) [ci skip] [ci skip] * New translations faq.md (Spanish) [ci skip] [ci skip] * New translations label.md (Spanish) [ci skip] [ci skip] * New translations stylus-support.md (Spanish) [ci skip] [ci skip] * New translations faq.md (Afrikaans) [ci skip] [ci skip] * New translations label.md (Afrikaans) [ci skip] [ci skip] * New translations stylus-support.md (Afrikaans) [ci skip] [ci skip] * New translations faq.md (Arabic) [ci skip] [ci skip] * New translations label.md (Arabic) [ci skip] [ci skip] * New translations stylus-support.md (Arabic) [ci skip] [ci skip] * New translations faq.md (Catalan) [ci skip] [ci skip] * New translations label.md (Catalan) [ci skip] [ci skip] * New translations stylus-support.md (Catalan) [ci skip] [ci skip] * New translations faq.md (Czech) [ci skip] [ci skip] * New translations label.md (Czech) [ci skip] [ci skip] * New translations stylus-support.md (Czech) [ci skip] [ci skip] * New translations faq.md (Danish) [ci skip] [ci skip] * New translations label.md (Danish) [ci skip] [ci skip] * New translations stylus-support.md (Danish) [ci skip] [ci skip] * New translations faq.md (German) [ci skip] [ci skip] * New translations label.md (German) [ci skip] [ci skip] * New translations stylus-support.md (German) [ci skip] [ci skip] * New translations faq.md (Greek) [ci skip] [ci skip] * New translations label.md (Greek) [ci skip] [ci skip] * New translations stylus-support.md (Greek) [ci skip] [ci skip] * New translations faq.md (Finnish) [ci skip] [ci skip] * New translations label.md (Finnish) [ci skip] [ci skip] * New translations stylus-support.md (Finnish) [ci skip] [ci skip] * New translations faq.md (Hebrew) [ci skip] [ci skip] * New translations label.md (Hebrew) [ci skip] [ci skip] * New translations stylus-support.md (Hebrew) [ci skip] [ci skip] * New translations faq.md (Hungarian) [ci skip] [ci skip] * New translations label.md (Hungarian) [ci skip] [ci skip] * New translations stylus-support.md (Hungarian) [ci skip] [ci skip] * New translations faq.md (Italian) [ci skip] [ci skip] * New translations label.md (Italian) [ci skip] [ci skip] * New translations stylus-support.md (Italian) [ci skip] [ci skip] * New translations faq.md (Japanese) [ci skip] [ci skip] * New translations label.md (Japanese) [ci skip] [ci skip] * New translations stylus-support.md (Japanese) [ci skip] [ci skip] * New translations faq.md (Korean) [ci skip] [ci skip] * New translations label.md (Korean) [ci skip] [ci skip] * New translations stylus-support.md (Korean) [ci skip] [ci skip] * New translations faq.md (Dutch) [ci skip] [ci skip] * New translations label.md (Dutch) [ci skip] [ci skip] * New translations stylus-support.md (Dutch) [ci skip] [ci skip] * New translations faq.md (Norwegian) [ci skip] [ci skip] * New translations label.md (Norwegian) [ci skip] [ci skip] * New translations stylus-support.md (Norwegian) [ci skip] [ci skip] * New translations faq.md (Polish) [ci skip] [ci skip] * New translations label.md (Polish) [ci skip] [ci skip] * New translations stylus-support.md (Polish) [ci skip] [ci skip] * New translations faq.md (Portuguese) [ci skip] [ci skip] * New translations label.md (Portuguese) [ci skip] [ci skip] * New translations stylus-support.md (Portuguese) [ci skip] [ci skip] * New translations faq.md (Russian) [ci skip] [ci skip] * New translations label.md (Russian) [ci skip] [ci skip] * New translations stylus-support.md (Russian) [ci skip] [ci skip] * New translations faq.md (Swedish) [ci skip] [ci skip] * New translations label.md (Swedish) [ci skip] [ci skip] * New translations stylus-support.md (Swedish) [ci skip] [ci skip] * New translations faq.md (Turkish) [ci skip] [ci skip] * New translations label.md (Turkish) [ci skip] [ci skip] * New translations stylus-support.md (Turkish) [ci skip] [ci skip] * New translations faq.md (Ukrainian) [ci skip] [ci skip] * New translations label.md (Ukrainian) [ci skip] [ci skip] * New translations stylus-support.md (Ukrainian) [ci skip] [ci skip] * New translations faq.md (Chinese Simplified) [ci skip] [ci skip] * New translations label.md (Chinese Simplified) [ci skip] [ci skip] * New translations stylus-support.md (Chinese Simplified) [ci skip] [ci skip] * New translations faq.md (Chinese Traditional) [ci skip] [ci skip] * New translations label.md (Chinese Traditional) [ci skip] [ci skip] * New translations stylus-support.md (Chinese Traditional) [ci skip] [ci skip] * New translations faq.md (Vietnamese) [ci skip] [ci skip] * New translations label.md (Vietnamese) [ci skip] [ci skip] * New translations stylus-support.md (Vietnamese) [ci skip] [ci skip] * New translations faq.md (Portuguese, Brazilian) [ci skip] [ci skip] * New translations label.md (Portuguese, Brazilian) [ci skip] [ci skip] * New translations stylus-support.md (Portuguese, Brazilian) [ci skip] [ci skip] * New translations faq.md (Indonesian) [ci skip] [ci skip] * New translations label.md (Indonesian) [ci skip] [ci skip] * New translations stylus-support.md (Indonesian) [ci skip] [ci skip] * New translations faq.md (Thai) [ci skip] [ci skip] * New translations label.md (Thai) [ci skip] [ci skip] * New translations stylus-support.md (Thai) [ci skip] [ci skip] * New translations faq.md (Hindi) [ci skip] [ci skip] * New translations label.md (Hindi) [ci skip] [ci skip] * New translations stylus-support.md (Hindi) [ci skip] [ci skip] * New translations faq.md (Odia) [ci skip] [ci skip] * New translations label.md (Odia) [ci skip] [ci skip] * New translations stylus-support.md (Odia) [ci skip] [ci skip] * New translations faq.md (Serbian) [ci skip] [ci skip] * New translations label.md (Serbian) [ci skip] [ci skip] * New translations stylus-support.md (Serbian) [ci skip] [ci skip] * New translations app_en.arb (Romanian) [ci skip] [ci skip] * New translations app_en.arb (French) [ci skip] [ci skip] * New translations app_en.arb (Spanish) [ci skip] [ci skip] * New translations app_en.arb (Arabic) [ci skip] [ci skip] * New translations app_en.arb (Czech) [ci skip] [ci skip] * New translations app_en.arb (Danish) [ci skip] [ci skip] * New translations app_en.arb (German) [ci skip] [ci skip] * New translations app_en.arb (Greek) [ci skip] [ci skip] * New translations app_en.arb (Finnish) [ci skip] [ci skip] * New translations app_en.arb (Italian) [ci skip] [ci skip] * New translations app_en.arb (Japanese) [ci skip] [ci skip] * New translations app_en.arb (Dutch) [ci skip] [ci skip] * New translations app_en.arb (Norwegian) [ci skip] [ci skip] * New translations app_en.arb (Polish) [ci skip] [ci skip] * New translations app_en.arb (Portuguese) [ci skip] [ci skip] * New translations app_en.arb (Russian) [ci skip] [ci skip] * New translations app_en.arb (Swedish) [ci skip] [ci skip] * New translations app_en.arb (Ukrainian) [ci skip] [ci skip] * New translations app_en.arb (Chinese Simplified) [ci skip] [ci skip] * New translations app_en.arb (Portuguese, Brazilian) [ci skip] [ci skip] * New translations app_en.arb (Indonesian) [ci skip] [ci skip] * New translations app_en.arb (Afrikaans) [ci skip] [ci skip] * New translations app_en.arb (Catalan) [ci skip] [ci skip] * New translations app_en.arb (Hebrew) [ci skip] [ci skip] * New translations app_en.arb (Hungarian) [ci skip] [ci skip] * New translations app_en.arb (Korean) [ci skip] [ci skip] * New translations app_en.arb (Turkish) [ci skip] [ci skip] * New translations app_en.arb (Chinese Traditional) [ci skip] [ci skip] * New translations app_en.arb (Vietnamese) [ci skip] [ci skip] * New translations app_en.arb (Thai) [ci skip] [ci skip] * New translations app_en.arb (Hindi) [ci skip] [ci skip] * New translations app_en.arb (Odia) [ci skip] [ci skip] * New translations app_en.arb (Serbian) [ci skip] [ci skip] * New translations onenote.md (Romanian) [ci skip] [ci skip] * New translations onenote.md (French) [ci skip] [ci skip] * New translations onenote.md (Spanish) [ci skip] [ci skip] * New translations onenote.md (Afrikaans) [ci skip] [ci skip] * New translations onenote.md (Arabic) [ci skip] [ci skip] * New translations onenote.md (Catalan) [ci skip] [ci skip] * New translations onenote.md (Czech) [ci skip] [ci skip] * New translations onenote.md (Danish) [ci skip] [ci skip] * New translations onenote.md (German) [ci skip] [ci skip] * New translations onenote.md (Greek) [ci skip] [ci skip] * New translations onenote.md (Finnish) [ci skip] [ci skip] * New translations onenote.md (Hebrew) [ci skip] [ci skip] * New translations onenote.md (Hungarian) [ci skip] [ci skip] * New translations onenote.md (Italian) [ci skip] [ci skip] * New translations onenote.md (Japanese) [ci skip] [ci skip] * New translations onenote.md (Korean) [ci skip] [ci skip] * New translations onenote.md (Dutch) [ci skip] [ci skip] * New translations onenote.md (Norwegian) [ci skip] [ci skip] * New translations onenote.md (Polish) [ci skip] [ci skip] * New translations onenote.md (Portuguese) [ci skip] [ci skip] * New translations onenote.md (Russian) [ci skip] [ci skip] * New translations onenote.md (Swedish) [ci skip] [ci skip] * New translations onenote.md (Turkish) [ci skip] [ci skip] * New translations onenote.md (Ukrainian) [ci skip] [ci skip] * New translations onenote.md (Chinese Simplified) [ci skip] [ci skip] * New translations onenote.md (Chinese Traditional) [ci skip] [ci skip] * New translations onenote.md (Vietnamese) [ci skip] [ci skip] * New translations onenote.md (Portuguese, Brazilian) [ci skip] [ci skip] * New translations onenote.md (Indonesian) [ci skip] [ci skip] * New translations onenote.md (Thai) [ci skip] [ci skip] * New translations onenote.md (Hindi) [ci skip] [ci skip] * New translations onenote.md (Odia) [ci skip] [ci skip] * New translations onenote.md (Serbian) [ci skip] [ci skip] * New translations app_en.arb (Romanian) [ci skip] [ci skip] * New translations app_en.arb (French) [ci skip] [ci skip] * New translations app_en.arb (Spanish) [ci skip] [ci skip] * New translations app_en.arb (Arabic) [ci skip] [ci skip] * New translations app_en.arb (Czech) [ci skip] [ci skip] * New translations app_en.arb (Afrikaans) [ci skip] [ci skip] * New translations app_en.arb (Catalan) [ci skip] [ci skip] * New translations app_en.arb (Danish) [ci skip] [ci skip] * New translations app_en.arb (German) [ci skip] [ci skip] * New translations app_en.arb (Greek) [ci skip] [ci skip] * New translations app_en.arb (Finnish) [ci skip] [ci skip] * New translations app_en.arb (Italian) [ci skip] [ci skip] * New translations app_en.arb (Japanese) [ci skip] [ci skip] * New translations app_en.arb (Dutch) [ci skip] [ci skip] * New translations app_en.arb (Norwegian) [ci skip] [ci skip] * New translations app_en.arb (Polish) [ci skip] [ci skip] * New translations app_en.arb (Portuguese) [ci skip] [ci skip] * New translations app_en.arb (Russian) [ci skip] [ci skip] * New translations app_en.arb (Swedish) [ci skip] [ci skip] * New translations app_en.arb (Ukrainian) [ci skip] [ci skip] * New translations app_en.arb (Chinese Simplified) [ci skip] [ci skip] * New translations app_en.arb (Portuguese, Brazilian) [ci skip] [ci skip] * New translations app_en.arb (Indonesian) [ci skip] [ci skip] * New translations app_en.arb (Hebrew) [ci skip] [ci skip] * New translations app_en.arb (Hungarian) [ci skip] [ci skip] * New translations app_en.arb (Korean) [ci skip] [ci skip] * New translations app_en.arb (Turkish) [ci skip] [ci skip] * New translations app_en.arb (Chinese Traditional) [ci skip] [ci skip] * New translations app_en.arb (Vietnamese) [ci skip] [ci skip] * New translations app_en.arb (Thai) [ci skip] [ci skip] * New translations app_en.arb (Hindi) [ci skip] [ci skip] * New translations app_en.arb (Odia) [ci skip] [ci skip] * New translations app_en.arb (Serbian) [ci skip] [ci skip] * New translations app_en.arb (Arabic) [ci skip] [ci skip] * New translations app_en.arb (Arabic) [ci skip] [ci skip] * New translations app_en.arb (Romanian) [ci skip] [ci skip] * New translations app_en.arb (French) [ci skip] [ci skip] * New translations app_en.arb (Spanish) [ci skip] [ci skip] * New translations app_en.arb (Czech) [ci skip] [ci skip] * New translations app_en.arb (Danish) [ci skip] [ci skip] * New translations app_en.arb (German) [ci skip] [ci skip] * New translations app_en.arb (Greek) [ci skip] [ci skip] * New translations app_en.arb (Finnish) [ci skip] [ci skip] * New translations app_en.arb (Italian) [ci skip] [ci skip] * New translations app_en.arb (Japanese) [ci skip] [ci skip] * New translations app_en.arb (Dutch) [ci skip] [ci skip] * New translations app_en.arb (Norwegian) [ci skip] [ci skip] * New translations app_en.arb (Polish) [ci skip] [ci skip] * New translations app_en.arb (Portuguese) [ci skip] [ci skip] * New translations app_en.arb (Russian) [ci skip] [ci skip] * New translations app_en.arb (Swedish) [ci skip] [ci skip] * New translations app_en.arb (Ukrainian) [ci skip] [ci skip] * New translations app_en.arb (Chinese Simplified) [ci skip] [ci skip] * New translations app_en.arb (Portuguese, Brazilian) [ci skip] [ci skip] * New translations app_en.arb (Arabic) [ci skip] [ci skip] --- app/lib/l10n/app_af.arb | 6 +- app/lib/l10n/app_ar.arb | 34 +- app/lib/l10n/app_ca.arb | 6 +- app/lib/l10n/app_cs.arb | 6 +- app/lib/l10n/app_da.arb | 6 +- app/lib/l10n/app_de.arb | 6 +- app/lib/l10n/app_el.arb | 6 +- app/lib/l10n/app_es.arb | 6 +- app/lib/l10n/app_fi.arb | 6 +- app/lib/l10n/app_fr.arb | 6 +- app/lib/l10n/app_he.arb | 6 +- app/lib/l10n/app_hi.arb | 6 +- app/lib/l10n/app_hu.arb | 6 +- app/lib/l10n/app_id.arb | 6 +- app/lib/l10n/app_it.arb | 6 +- app/lib/l10n/app_ja.arb | 6 +- app/lib/l10n/app_ko.arb | 6 +- app/lib/l10n/app_nl.arb | 6 +- app/lib/l10n/app_no.arb | 6 +- app/lib/l10n/app_or.arb | 6 +- app/lib/l10n/app_pl.arb | 6 +- app/lib/l10n/app_pt.arb | 6 +- app/lib/l10n/app_pt_BR.arb | 6 +- app/lib/l10n/app_ro.arb | 6 +- app/lib/l10n/app_ru.arb | 6 +- app/lib/l10n/app_sr.arb | 6 +- app/lib/l10n/app_sv.arb | 6 +- app/lib/l10n/app_th.arb | 6 +- app/lib/l10n/app_tr.arb | 6 +- app/lib/l10n/app_uk.arb | 6 +- app/lib/l10n/app_vi.arb | 6 +- app/lib/l10n/app_zh-Hant.arb | 6 +- app/lib/l10n/app_zh.arb | 6 +- docs/src/content/docs/af/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/ar/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/ca/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/cs/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/da/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/de/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/el/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/es/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/fi/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/fr/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/he/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/hi/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/hu/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/id/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/it/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/ja/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/ko/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/nl/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/no/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/or/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/pl/docs/v2/onenote.md | 297 ++++++++++++++++++ .../src/content/docs/pt-br/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/pt/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/ro/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/ru/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/sr/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/sv/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/th/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/tr/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/uk/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/vi/docs/v2/onenote.md | 297 ++++++++++++++++++ .../content/docs/zh-hant/docs/v2/onenote.md | 297 ++++++++++++++++++ docs/src/content/docs/zh/docs/v2/onenote.md | 297 ++++++++++++++++++ 66 files changed, 9980 insertions(+), 47 deletions(-) create mode 100644 docs/src/content/docs/af/docs/v2/onenote.md create mode 100644 docs/src/content/docs/ar/docs/v2/onenote.md create mode 100644 docs/src/content/docs/ca/docs/v2/onenote.md create mode 100644 docs/src/content/docs/cs/docs/v2/onenote.md create mode 100644 docs/src/content/docs/da/docs/v2/onenote.md create mode 100644 docs/src/content/docs/de/docs/v2/onenote.md create mode 100644 docs/src/content/docs/el/docs/v2/onenote.md create mode 100644 docs/src/content/docs/es/docs/v2/onenote.md create mode 100644 docs/src/content/docs/fi/docs/v2/onenote.md create mode 100644 docs/src/content/docs/fr/docs/v2/onenote.md create mode 100644 docs/src/content/docs/he/docs/v2/onenote.md create mode 100644 docs/src/content/docs/hi/docs/v2/onenote.md create mode 100644 docs/src/content/docs/hu/docs/v2/onenote.md create mode 100644 docs/src/content/docs/id/docs/v2/onenote.md create mode 100644 docs/src/content/docs/it/docs/v2/onenote.md create mode 100644 docs/src/content/docs/ja/docs/v2/onenote.md create mode 100644 docs/src/content/docs/ko/docs/v2/onenote.md create mode 100644 docs/src/content/docs/nl/docs/v2/onenote.md create mode 100644 docs/src/content/docs/no/docs/v2/onenote.md create mode 100644 docs/src/content/docs/or/docs/v2/onenote.md create mode 100644 docs/src/content/docs/pl/docs/v2/onenote.md create mode 100644 docs/src/content/docs/pt-br/docs/v2/onenote.md create mode 100644 docs/src/content/docs/pt/docs/v2/onenote.md create mode 100644 docs/src/content/docs/ro/docs/v2/onenote.md create mode 100644 docs/src/content/docs/ru/docs/v2/onenote.md create mode 100644 docs/src/content/docs/sr/docs/v2/onenote.md create mode 100644 docs/src/content/docs/sv/docs/v2/onenote.md create mode 100644 docs/src/content/docs/th/docs/v2/onenote.md create mode 100644 docs/src/content/docs/tr/docs/v2/onenote.md create mode 100644 docs/src/content/docs/uk/docs/v2/onenote.md create mode 100644 docs/src/content/docs/vi/docs/v2/onenote.md create mode 100644 docs/src/content/docs/zh-hant/docs/v2/onenote.md create mode 100644 docs/src/content/docs/zh/docs/v2/onenote.md diff --git a/app/lib/l10n/app_af.arb b/app/lib/l10n/app_af.arb index b776f08c20f7..796c9c510a48 100644 --- a/app/lib/l10n/app_af.arb +++ b/app/lib/l10n/app_af.arb @@ -99,6 +99,7 @@ }, "background": "Agtergrond", "applyBackground": "Apply background", + "applyAreas": "Apply areas", "currentPage": "Current page", "allPages": "All pages", "box": "Boks", @@ -123,6 +124,7 @@ }, "defaultPalette": "Verstek palet", "highlighter": "Merkpen", + "combinePaths": "Combine paths", "add": "Voeg by", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Hold a key to temporarily switch to another tool. Releasing the key switches back to the previous tool.", "key": "Key", "bringMovedElementsToFront": "Bring moved elements to front", - "addTool": "Add tool" + "addTool": "Add tool", + "nextPage": "Next page", + "previousPage": "Previous page" } \ No newline at end of file diff --git a/app/lib/l10n/app_ar.arb b/app/lib/l10n/app_ar.arb index ce56d9157788..86e55e452c13 100644 --- a/app/lib/l10n/app_ar.arb +++ b/app/lib/l10n/app_ar.arb @@ -99,6 +99,7 @@ }, "background": "الخلفية", "applyBackground": "تطبيق الخلفية", + "applyAreas": "تطبيق المناطق", "currentPage": "الصفحة الحالية", "allPages": "جميع الصفحات", "box": "مربع", @@ -123,6 +124,7 @@ }, "defaultPalette": "اللوحة الافتراضية", "highlighter": "قلم تظليل", + "combinePaths": "دمج المسارات", "add": "إضافة", "@add": { "description": "Add action" @@ -147,13 +149,13 @@ "description": "Style property" }, "solid": "صلب", - "solidColor": "لون صلب", + "solidColor": "لون معتم", "gradient": "متدرج", "gradientType": "نوع التدرج", "linear": "خطي", - "radial": "Radial", - "tint": "Tint", - "blur": "Blur", + "radial": "تدرّج شعاعي", + "tint": "تلوين", + "blur": "تمويه", "double": "مزدوج", "dotted": "منقط", "dottedDark": "منقط داكن", @@ -299,7 +301,7 @@ "notSet": "غير محدد", "enterLayer": "أدخل اسم الطبقة", "eraseShapes": "مسح الأشكال", - "eraseShapeModeNone": "لا تمحو الأشكال", + "eraseShapeModeNone": "لا تمح الأشكال", "eraseShapeModeTouchEdges": "محو عند لمس الحواف", "eraseShapeModeTouchAnywhere": "محو عند لمس أي مكان", "selectElementModeTouchEdges": "حدد عند لمس الحواف", @@ -964,7 +966,7 @@ "ascending": "تصاعدي", "descending": "تنازلي", "imageScale": "حجم الصورة", - "svgScale": "مقياس SVG", + "svgScale": "مقاس SVG", "noImageSelected": "لم يتم تحديد صورة", "noSvgSelected": "لم يتم تحديد SVG", "select": "حدد", @@ -1237,13 +1239,13 @@ "description": "Offset property" }, "end": "نهاية", - "radius": "Radius", - "focalPoint": "مركز التنسيق", - "useOffCenterStartPoint": "استخدام نقطة البداية خارج المركز", - "focal": "Focal", - "focalRadius": "Focal radius", - "colorStops": "إيقاف الألوان", - "addStop": "إضافة إيقاف", + "radius": "نصف القطر", + "focalPoint": "نقطة التركيز", + "useOffCenterStartPoint": "استخدم نقطة بداية مغايرة للمركز", + "focal": "البؤرة", + "focalRadius": "نصف القطر البؤري", + "colorStops": "نقاط الألوان", + "addStop": "إضافة نقطة", "positionDependent": "يعتمد على الموقع", "flipHorizontal": "قلب أفقي", "flipVertical": "قلب عمودي", @@ -1366,6 +1368,8 @@ "holdShortcuts": "الاختصارات المؤقتة", "holdShortcutsDescription": "\"الاختصار المؤقت\" يُفعّل الأداة المحددة خلال الضغط عليه، ويعود إلى الأداة السابقة عند تركه.", "key": "المفتاح", - "bringMovedElementsToFront": "إحضار العناصر إلى الأمام", - "addTool": "إضافة أداة" + "bringMovedElementsToFront": "إحضار العناصر المنقولة إلى المقدمة", + "addTool": "إضافة أداة", + "nextPage": "الصفحة التالية", + "previousPage": "الصفحة السابقة" } \ No newline at end of file diff --git a/app/lib/l10n/app_ca.arb b/app/lib/l10n/app_ca.arb index 94219d98157e..8818a4b5d2e0 100644 --- a/app/lib/l10n/app_ca.arb +++ b/app/lib/l10n/app_ca.arb @@ -99,6 +99,7 @@ }, "background": "Fons", "applyBackground": "Apply background", + "applyAreas": "Apply areas", "currentPage": "Current page", "allPages": "All pages", "box": "Quadre", @@ -123,6 +124,7 @@ }, "defaultPalette": "Paleta per defecte", "highlighter": "Marcador", + "combinePaths": "Combine paths", "add": "Afegeix", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Hold a key to temporarily switch to another tool. Releasing the key switches back to the previous tool.", "key": "Key", "bringMovedElementsToFront": "Bring moved elements to front", - "addTool": "Add tool" + "addTool": "Add tool", + "nextPage": "Next page", + "previousPage": "Previous page" } \ No newline at end of file diff --git a/app/lib/l10n/app_cs.arb b/app/lib/l10n/app_cs.arb index a19b6287f5ea..c2f0a356c893 100644 --- a/app/lib/l10n/app_cs.arb +++ b/app/lib/l10n/app_cs.arb @@ -99,6 +99,7 @@ }, "background": "Pozadí", "applyBackground": "Použít pozadí", + "applyAreas": "Aplikovat oblasti", "currentPage": "Aktuální stránka", "allPages": "Všechny stránky", "box": "Krabice", @@ -123,6 +124,7 @@ }, "defaultPalette": "Výchozí paleta", "highlighter": "Zvýrazňovač", + "combinePaths": "Kombinovat cesty", "add": "Přidat", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Podržte klávesu pro dočasné přepnutí na jiný nástroj. Zmáčkněte klávesu zpět na předchozí nástroj.", "key": "Klíč", "bringMovedElementsToFront": "Přeneste přesunuté prvky do předku", - "addTool": "Přidat nástroj" + "addTool": "Přidat nástroj", + "nextPage": "Další stránka", + "previousPage": "Předchozí stránka" } \ No newline at end of file diff --git a/app/lib/l10n/app_da.arb b/app/lib/l10n/app_da.arb index 18e2a99911da..5f425955b052 100644 --- a/app/lib/l10n/app_da.arb +++ b/app/lib/l10n/app_da.arb @@ -99,6 +99,7 @@ }, "background": "Baggrund", "applyBackground": "Anvend baggrund", + "applyAreas": "Anvend områder", "currentPage": "Aktuel side", "allPages": "Alle sider", "box": "Boks", @@ -123,6 +124,7 @@ }, "defaultPalette": "Standard palet", "highlighter": "Fremhævning", + "combinePaths": "Kombiner stier", "add": "Tilføj", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Hold en tast nede for midlertidigt at skifte til et andet værktøj. Frigive tasten skifter tilbage til det forrige værktøj.", "key": "Nøgle", "bringMovedElementsToFront": "Bring flyttede elementer foran", - "addTool": "Tilføj værktøj" + "addTool": "Tilføj værktøj", + "nextPage": "Næste side", + "previousPage": "Forrige side" } \ No newline at end of file diff --git a/app/lib/l10n/app_de.arb b/app/lib/l10n/app_de.arb index 5199003fe7f1..838e486c71c2 100644 --- a/app/lib/l10n/app_de.arb +++ b/app/lib/l10n/app_de.arb @@ -99,6 +99,7 @@ }, "background": "Hintergrund", "applyBackground": "Hintergrund anwenden", + "applyAreas": "Bereiche anwenden", "currentPage": "Aktuelle Seite", "allPages": "Alle Seiten", "box": "Kasten", @@ -123,6 +124,7 @@ }, "defaultPalette": "Standardpalette", "highlighter": "Hervorhebung", + "combinePaths": "Pfade kombinieren", "add": "Neu", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Halte eine Taste um vorübergehend zu einem anderen Werkzeug zu wechseln. Wenn die Taste losgelassen wird, schaltet er zurück zum vorherigen Werkzeug.", "key": "Schlüssel", "bringMovedElementsToFront": "Bewege Elemente nach vorne", - "addTool": "Werkzeug hinzufügen" + "addTool": "Werkzeug hinzufügen", + "nextPage": "Nächste Seite", + "previousPage": "Vorherige Seite" } \ No newline at end of file diff --git a/app/lib/l10n/app_el.arb b/app/lib/l10n/app_el.arb index d6398ccfa2d6..610f5230ab7c 100644 --- a/app/lib/l10n/app_el.arb +++ b/app/lib/l10n/app_el.arb @@ -99,6 +99,7 @@ }, "background": "Φόντο", "applyBackground": "Εφαρμογή φόντου", + "applyAreas": "Εφαρμογή περιοχών", "currentPage": "Τρέχουσα σελίδα", "allPages": "Όλες οι σελίδες", "box": "Κουτί", @@ -123,6 +124,7 @@ }, "defaultPalette": "Προεπιλεγμένη παλέτα", "highlighter": "Επισήμανση", + "combinePaths": "Συνδυάστε διαδρομές", "add": "Προσθήκη", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Κρατήστε ένα κλειδί για να μεταβείτε προσωρινά σε ένα άλλο εργαλείο.", "key": "Κλειδί", "bringMovedElementsToFront": "Φέρτε τα μετακινούμενα στοιχεία μπροστά", - "addTool": "Προσθήκη εργαλείου" + "addTool": "Προσθήκη εργαλείου", + "nextPage": "Επόμενη σελίδα", + "previousPage": "Προηγούμενη σελίδα" } \ No newline at end of file diff --git a/app/lib/l10n/app_es.arb b/app/lib/l10n/app_es.arb index b1ee2abcba73..ca9b21717ca6 100644 --- a/app/lib/l10n/app_es.arb +++ b/app/lib/l10n/app_es.arb @@ -99,6 +99,7 @@ }, "background": "Fondo", "applyBackground": "Aplicar fondo", + "applyAreas": "Aplicar áreas", "currentPage": "Página actual", "allPages": "Todas las páginas", "box": "Caja", @@ -123,6 +124,7 @@ }, "defaultPalette": "Paleta por defecto", "highlighter": "Resaltado", + "combinePaths": "Combinar rutas", "add": "Añadir", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Mantenga pulsada una tecla para cambiar temporalmente a otra herramienta. Liberar la tecla vuelve a la herramienta anterior.", "key": "Clave", "bringMovedElementsToFront": "Trae los elementos movidos al frente", - "addTool": "Añadir herramienta" + "addTool": "Añadir herramienta", + "nextPage": "Página siguiente", + "previousPage": "Página anterior" } \ No newline at end of file diff --git a/app/lib/l10n/app_fi.arb b/app/lib/l10n/app_fi.arb index 7e997fbbc4f4..57af219c4811 100644 --- a/app/lib/l10n/app_fi.arb +++ b/app/lib/l10n/app_fi.arb @@ -99,6 +99,7 @@ }, "background": "Tausta", "applyBackground": "Käytä taustakuvaa", + "applyAreas": "Käytä alueita", "currentPage": "Nykyinen sivu", "allPages": "Kaikki sivut", "box": "Laatikko", @@ -123,6 +124,7 @@ }, "defaultPalette": "Oletus paletti", "highlighter": "Korostus", + "combinePaths": "Yhdistä polut", "add": "Lisää", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Pidä näppäintä pohjassa vaihtaaksesi väliaikaisesti toiseen työkaluun. Avaimen vapauttaminen siirtyy takaisin edelliseen työkaluun.", "key": "Avain", "bringMovedElementsToFront": "Tuo siirretyt elementit eteen", - "addTool": "Lisää työkalu" + "addTool": "Lisää työkalu", + "nextPage": "Seuraava sivu", + "previousPage": "Edellinen sivu" } \ No newline at end of file diff --git a/app/lib/l10n/app_fr.arb b/app/lib/l10n/app_fr.arb index d7eb73982917..1c5be5593545 100644 --- a/app/lib/l10n/app_fr.arb +++ b/app/lib/l10n/app_fr.arb @@ -99,6 +99,7 @@ }, "background": "Arrière-plan", "applyBackground": "Appliquer l'arrière-plan", + "applyAreas": "Appliquer des zones", "currentPage": "Page actuelle", "allPages": "Toutes les pages", "box": "Boîte", @@ -123,6 +124,7 @@ }, "defaultPalette": "Palette par défaut", "highlighter": "Surligneur", + "combinePaths": "Combiner les chemins", "add": "Ajouter", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Maintenez une touche enfoncée pour basculer temporairement vers un autre outil. Relâchez les commutateurs de touches vers l'outil précédent.", "key": "Clés", "bringMovedElementsToFront": "Apporter les éléments déplacés à l'avant", - "addTool": "Ajouter un outil" + "addTool": "Ajouter un outil", + "nextPage": "Page suivante", + "previousPage": "Page précédente" } \ No newline at end of file diff --git a/app/lib/l10n/app_he.arb b/app/lib/l10n/app_he.arb index 5b1def05c600..cbf245ffaf22 100644 --- a/app/lib/l10n/app_he.arb +++ b/app/lib/l10n/app_he.arb @@ -99,6 +99,7 @@ }, "background": "רקע", "applyBackground": "Apply background", + "applyAreas": "Apply areas", "currentPage": "Current page", "allPages": "All pages", "box": "תיבה", @@ -123,6 +124,7 @@ }, "defaultPalette": "פלטת צבעים ברירת מחדל", "highlighter": "מרקר", + "combinePaths": "Combine paths", "add": "הוספה", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Hold a key to temporarily switch to another tool. Releasing the key switches back to the previous tool.", "key": "Key", "bringMovedElementsToFront": "Bring moved elements to front", - "addTool": "Add tool" + "addTool": "Add tool", + "nextPage": "Next page", + "previousPage": "Previous page" } \ No newline at end of file diff --git a/app/lib/l10n/app_hi.arb b/app/lib/l10n/app_hi.arb index 33f2e0792474..aef62f74eb32 100644 --- a/app/lib/l10n/app_hi.arb +++ b/app/lib/l10n/app_hi.arb @@ -99,6 +99,7 @@ }, "background": "पृष्ठभूमि", "applyBackground": "Apply background", + "applyAreas": "Apply areas", "currentPage": "Current page", "allPages": "All pages", "box": "बॉक्स", @@ -123,6 +124,7 @@ }, "defaultPalette": "डिफ़ॉल्ट पैलेट", "highlighter": "हाइलाइटर", + "combinePaths": "Combine paths", "add": "जोड़ें", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Hold a key to temporarily switch to another tool. Releasing the key switches back to the previous tool.", "key": "Key", "bringMovedElementsToFront": "Bring moved elements to front", - "addTool": "Add tool" + "addTool": "Add tool", + "nextPage": "Next page", + "previousPage": "Previous page" } \ No newline at end of file diff --git a/app/lib/l10n/app_hu.arb b/app/lib/l10n/app_hu.arb index 1e8802b46929..b3bb4b8593aa 100644 --- a/app/lib/l10n/app_hu.arb +++ b/app/lib/l10n/app_hu.arb @@ -99,6 +99,7 @@ }, "background": "Háttér", "applyBackground": "Apply background", + "applyAreas": "Apply areas", "currentPage": "Current page", "allPages": "All pages", "box": "Doboz", @@ -123,6 +124,7 @@ }, "defaultPalette": "Alapértelmezett paletta", "highlighter": "Kiemelés", + "combinePaths": "Combine paths", "add": "Hozzáadás", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Hold a key to temporarily switch to another tool. Releasing the key switches back to the previous tool.", "key": "Key", "bringMovedElementsToFront": "Bring moved elements to front", - "addTool": "Add tool" + "addTool": "Add tool", + "nextPage": "Next page", + "previousPage": "Previous page" } \ No newline at end of file diff --git a/app/lib/l10n/app_id.arb b/app/lib/l10n/app_id.arb index bd9e197f8953..cd58a7ca3a61 100644 --- a/app/lib/l10n/app_id.arb +++ b/app/lib/l10n/app_id.arb @@ -99,6 +99,7 @@ }, "background": "Latar belakang", "applyBackground": "Apply background", + "applyAreas": "Apply areas", "currentPage": "Current page", "allPages": "All pages", "box": "Kotak", @@ -123,6 +124,7 @@ }, "defaultPalette": "Palet bawaan", "highlighter": "Penyorot", + "combinePaths": "Combine paths", "add": "Tambah", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Tahan tombol untuk sementara beralih ke alat lain. Melepas tombol akan kembali ke alat sebelumnya.", "key": "Tombol", "bringMovedElementsToFront": "Bring moved elements to front", - "addTool": "Add tool" + "addTool": "Add tool", + "nextPage": "Next page", + "previousPage": "Previous page" } \ No newline at end of file diff --git a/app/lib/l10n/app_it.arb b/app/lib/l10n/app_it.arb index d359dc2a9c84..a22d9ed2a74a 100644 --- a/app/lib/l10n/app_it.arb +++ b/app/lib/l10n/app_it.arb @@ -99,6 +99,7 @@ }, "background": "Sfondo", "applyBackground": "Applica sfondo", + "applyAreas": "Applica aree", "currentPage": "Pagina corrente", "allPages": "Tutte le pagine", "box": "Riquadro", @@ -123,6 +124,7 @@ }, "defaultPalette": "Palette predefinita", "highlighter": "Evidenziatore", + "combinePaths": "Combina percorsi", "add": "Aggiungi", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Tieni premuto un tasto per passare temporaneamente a un altro strumento. Rilasciare il tasto passa allo strumento precedente.", "key": "Chiave", "bringMovedElementsToFront": "Porta in primo piano gli elementi spostati", - "addTool": "Aggiungi strumento" + "addTool": "Aggiungi strumento", + "nextPage": "Pagina successiva", + "previousPage": "Pagina precedente" } \ No newline at end of file diff --git a/app/lib/l10n/app_ja.arb b/app/lib/l10n/app_ja.arb index c14904468a4b..4049fce44432 100644 --- a/app/lib/l10n/app_ja.arb +++ b/app/lib/l10n/app_ja.arb @@ -99,6 +99,7 @@ }, "background": "背景", "applyBackground": "背景を適用", + "applyAreas": "エリアを適用", "currentPage": "現在のページ", "allPages": "すべてのページ", "box": "Box", @@ -123,6 +124,7 @@ }, "defaultPalette": "デフォルトのパレット", "highlighter": "ハイライト", + "combinePaths": "パスを結合", "add": "追加", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "キーを押して別のツールに一時的に切り替えます。キースイッチを前のツールに戻します。", "key": "キー", "bringMovedElementsToFront": "移動した要素を前面に移動", - "addTool": "ツールを追加" + "addTool": "ツールを追加", + "nextPage": "次のページ", + "previousPage": "前のページ" } \ No newline at end of file diff --git a/app/lib/l10n/app_ko.arb b/app/lib/l10n/app_ko.arb index b47c96cc0581..c8fc51ef035b 100644 --- a/app/lib/l10n/app_ko.arb +++ b/app/lib/l10n/app_ko.arb @@ -99,6 +99,7 @@ }, "background": "배경", "applyBackground": "Apply background", + "applyAreas": "Apply areas", "currentPage": "Current page", "allPages": "All pages", "box": "상자", @@ -123,6 +124,7 @@ }, "defaultPalette": "기본 팔레트", "highlighter": "형광펜", + "combinePaths": "Combine paths", "add": "추가", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Hold a key to temporarily switch to another tool. Releasing the key switches back to the previous tool.", "key": "Key", "bringMovedElementsToFront": "Bring moved elements to front", - "addTool": "Add tool" + "addTool": "Add tool", + "nextPage": "Next page", + "previousPage": "Previous page" } \ No newline at end of file diff --git a/app/lib/l10n/app_nl.arb b/app/lib/l10n/app_nl.arb index 369f54ae6eb9..33ddaaffaa95 100644 --- a/app/lib/l10n/app_nl.arb +++ b/app/lib/l10n/app_nl.arb @@ -99,6 +99,7 @@ }, "background": "Achtergrond", "applyBackground": "Achtergrond toepassen", + "applyAreas": "Pas gebieden toe", "currentPage": "Huidige pagina", "allPages": "Alle pagina's", "box": "Vierkant", @@ -123,6 +124,7 @@ }, "defaultPalette": "Standaard palet", "highlighter": "Markeerstift", + "combinePaths": "Combineer paden", "add": "Toevoegen", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Houd een sleutel ingedrukt om tijdelijk naar een andere tool over te schakelen. Laat de toetsschakelaars terug naar de vorige tool.", "key": "Sleutel", "bringMovedElementsToFront": "Breng verplaatste elementen naar voren", - "addTool": "Functie toevoegen" + "addTool": "Functie toevoegen", + "nextPage": "Volgende pagina", + "previousPage": "Vorige pagina" } \ No newline at end of file diff --git a/app/lib/l10n/app_no.arb b/app/lib/l10n/app_no.arb index e9209ccb11ab..b521d60d89f7 100644 --- a/app/lib/l10n/app_no.arb +++ b/app/lib/l10n/app_no.arb @@ -99,6 +99,7 @@ }, "background": "Bakgrunn", "applyBackground": "Bruk bakgrunn", + "applyAreas": "Påfør områder", "currentPage": "Nåværende side", "allPages": "Alle sider", "box": "Boks", @@ -123,6 +124,7 @@ }, "defaultPalette": "Standard palett", "highlighter": "Uthever", + "combinePaths": "Kombiner baner", "add": "Legg til", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Hold en nøkkel for å midlertidig bytte til et annet verktøy. Løs inn tasten skifter tilbake til det forrige verktøyet.", "key": "Nøkkel", "bringMovedElementsToFront": "Plasser flyttede elementer forsiden", - "addTool": "Legg til verktøy" + "addTool": "Legg til verktøy", + "nextPage": "Neste side", + "previousPage": "Forrige side" } \ No newline at end of file diff --git a/app/lib/l10n/app_or.arb b/app/lib/l10n/app_or.arb index 34fd035331b9..3e7dc62aff79 100644 --- a/app/lib/l10n/app_or.arb +++ b/app/lib/l10n/app_or.arb @@ -99,6 +99,7 @@ }, "background": "ପୃଷ୍ଠଭୂମି", "applyBackground": "Apply background", + "applyAreas": "Apply areas", "currentPage": "Current page", "allPages": "All pages", "box": "ପେଟିକା", @@ -123,6 +124,7 @@ }, "defaultPalette": "ଡିଫଲ୍ଟ ପ୍ୟାଲେଟ୍", "highlighter": "ହାଇଲାଇଟର୍", + "combinePaths": "Combine paths", "add": "ଯୋଗ କରନ୍ତୁ", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Hold a key to temporarily switch to another tool. Releasing the key switches back to the previous tool.", "key": "Key", "bringMovedElementsToFront": "Bring moved elements to front", - "addTool": "Add tool" + "addTool": "Add tool", + "nextPage": "Next page", + "previousPage": "Previous page" } \ No newline at end of file diff --git a/app/lib/l10n/app_pl.arb b/app/lib/l10n/app_pl.arb index 83977c7fedbc..28e8f8d5e004 100644 --- a/app/lib/l10n/app_pl.arb +++ b/app/lib/l10n/app_pl.arb @@ -99,6 +99,7 @@ }, "background": "Kontekst", "applyBackground": "Zastosuj tło", + "applyAreas": "Zastosuj obszary", "currentPage": "Bieżąca strona", "allPages": "Wszystkie strony", "box": "Pudełko", @@ -123,6 +124,7 @@ }, "defaultPalette": "Domyślna paleta", "highlighter": "Podświetlenie", + "combinePaths": "Połącz ścieżki", "add": "Dodaj", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Przytrzymaj klawisz, aby tymczasowo przełączyć się na inne narzędzie. Uruchomienie przełącznika klawisza przywróci poprzednie narzędzie.", "key": "Klucz", "bringMovedElementsToFront": "Przynieś elementy do przodu", - "addTool": "Dodaj narzędzie" + "addTool": "Dodaj narzędzie", + "nextPage": "Następna strona", + "previousPage": "Poprzednia strona" } \ No newline at end of file diff --git a/app/lib/l10n/app_pt.arb b/app/lib/l10n/app_pt.arb index b6899408e09c..09c4b336969b 100644 --- a/app/lib/l10n/app_pt.arb +++ b/app/lib/l10n/app_pt.arb @@ -99,6 +99,7 @@ }, "background": "Fundo", "applyBackground": "Aplicar fundo", + "applyAreas": "Aplicar áreas", "currentPage": "Página atual", "allPages": "Todas as páginas", "box": "Caixa", @@ -123,6 +124,7 @@ }, "defaultPalette": "Paleta padrão", "highlighter": "Marcador", + "combinePaths": "Combinar caminhos", "add": "Adicionar", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Segure uma tecla para alternar temporariamente para outra ferramenta. Liberando a tecla de volta para a ferramenta anterior.", "key": "Chave", "bringMovedElementsToFront": "Trazer elementos movidos para a frente", - "addTool": "Adicionar ferramenta" + "addTool": "Adicionar ferramenta", + "nextPage": "Página seguinte", + "previousPage": "Página anterior" } \ No newline at end of file diff --git a/app/lib/l10n/app_pt_BR.arb b/app/lib/l10n/app_pt_BR.arb index 47a33c36e1a9..ff528df2cf69 100644 --- a/app/lib/l10n/app_pt_BR.arb +++ b/app/lib/l10n/app_pt_BR.arb @@ -99,6 +99,7 @@ }, "background": "Plano de fundo", "applyBackground": "Aplicar fundo", + "applyAreas": "Aplicar áreas", "currentPage": "Página atual", "allPages": "Todas as páginas", "box": "Caixa", @@ -123,6 +124,7 @@ }, "defaultPalette": "Paleta padrão", "highlighter": "Destaque", + "combinePaths": "Combinar caminhos", "add": "Adicionar", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Segure uma tecla para alternar temporariamente para outra ferramenta. Liberando a tecla de volta para a ferramenta anterior.", "key": "Chave", "bringMovedElementsToFront": "Trazer elementos movidos para a frente", - "addTool": "Adicionar ferramenta" + "addTool": "Adicionar ferramenta", + "nextPage": "Página seguinte", + "previousPage": "Página anterior" } \ No newline at end of file diff --git a/app/lib/l10n/app_ro.arb b/app/lib/l10n/app_ro.arb index 141c6592c31f..d03ec2b22cd0 100644 --- a/app/lib/l10n/app_ro.arb +++ b/app/lib/l10n/app_ro.arb @@ -99,6 +99,7 @@ }, "background": "Context", "applyBackground": "Aplică fundal", + "applyAreas": "Aplică zone", "currentPage": "Pagina curentă", "allPages": "Toate paginile", "box": "Cutie", @@ -123,6 +124,7 @@ }, "defaultPalette": "Paletă implicită", "highlighter": "Evidențiere", + "combinePaths": "Combinați căile", "add": "Adăugare", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Țineți apăsată o cheie pentru a trece temporar la un alt instrument. Lansând tasta se schimbă înapoi la unealta anterioară.", "key": "Cheie", "bringMovedElementsToFront": "Aduce elementele mutate în față", - "addTool": "Adăugare unealtă" + "addTool": "Adăugare unealtă", + "nextPage": "Pagina următoare", + "previousPage": "Pagina precedentă" } \ No newline at end of file diff --git a/app/lib/l10n/app_ru.arb b/app/lib/l10n/app_ru.arb index 885e3629a87f..ce72937dd49e 100644 --- a/app/lib/l10n/app_ru.arb +++ b/app/lib/l10n/app_ru.arb @@ -99,6 +99,7 @@ }, "background": "Фон", "applyBackground": "Применить фон", + "applyAreas": "Применить области", "currentPage": "Текущая страница", "allPages": "Все страницы", "box": "Блок", @@ -123,6 +124,7 @@ }, "defaultPalette": "Стандартная палитра", "highlighter": "Выделение", + "combinePaths": "Комбинировать пути", "add": "Добавить", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Удерживайте клавишу для временного переключения на другой инструмент. Отпустите клавишу обратно на предыдущий инструмент.", "key": "Спецификация", "bringMovedElementsToFront": "Принести перемещенные элементы на передний план", - "addTool": "Добавить инструмент" + "addTool": "Добавить инструмент", + "nextPage": "Следующая страница", + "previousPage": "Предыдущая страница" } \ No newline at end of file diff --git a/app/lib/l10n/app_sr.arb b/app/lib/l10n/app_sr.arb index 932d289d06f9..418b1370b9d5 100644 --- a/app/lib/l10n/app_sr.arb +++ b/app/lib/l10n/app_sr.arb @@ -99,6 +99,7 @@ }, "background": "Pozadina", "applyBackground": "Apply background", + "applyAreas": "Apply areas", "currentPage": "Current page", "allPages": "All pages", "box": "Kutija", @@ -123,6 +124,7 @@ }, "defaultPalette": "Podrazumevana paleta", "highlighter": "Marker", + "combinePaths": "Combine paths", "add": "Dodaj", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Hold a key to temporarily switch to another tool. Releasing the key switches back to the previous tool.", "key": "Key", "bringMovedElementsToFront": "Bring moved elements to front", - "addTool": "Add tool" + "addTool": "Add tool", + "nextPage": "Next page", + "previousPage": "Previous page" } \ No newline at end of file diff --git a/app/lib/l10n/app_sv.arb b/app/lib/l10n/app_sv.arb index 997d5dffde56..7d8c2f32494d 100644 --- a/app/lib/l10n/app_sv.arb +++ b/app/lib/l10n/app_sv.arb @@ -99,6 +99,7 @@ }, "background": "Bakgrund", "applyBackground": "Tillämpa bakgrund", + "applyAreas": "Tillämpa områden", "currentPage": "Nuvarande sida", "allPages": "Alla sidor", "box": "Låda", @@ -123,6 +124,7 @@ }, "defaultPalette": "Standard palett", "highlighter": "Highlighter", + "combinePaths": "Kombinera sökvägar", "add": "Lägg till", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Håll en nyckel för att tillfälligt växla till ett annat verktyg. Släppa knapparna tillbaka till föregående verktyg.", "key": "Nyckel", "bringMovedElementsToFront": "Ta med flyttade element framtill", - "addTool": "Lägg till verktyg" + "addTool": "Lägg till verktyg", + "nextPage": "Nästa sida", + "previousPage": "Föregående sida" } \ No newline at end of file diff --git a/app/lib/l10n/app_th.arb b/app/lib/l10n/app_th.arb index 53e23ee25f1b..0e20d8f6eab0 100644 --- a/app/lib/l10n/app_th.arb +++ b/app/lib/l10n/app_th.arb @@ -99,6 +99,7 @@ }, "background": "พื้นหลัง", "applyBackground": "Apply background", + "applyAreas": "Apply areas", "currentPage": "Current page", "allPages": "All pages", "box": "กล่อง", @@ -123,6 +124,7 @@ }, "defaultPalette": "พาเลตเริ่มต้น", "highlighter": "ปากกาเน้นข้อความ", + "combinePaths": "Combine paths", "add": "เพิ่ม", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Hold a key to temporarily switch to another tool. Releasing the key switches back to the previous tool.", "key": "Key", "bringMovedElementsToFront": "Bring moved elements to front", - "addTool": "Add tool" + "addTool": "Add tool", + "nextPage": "Next page", + "previousPage": "Previous page" } \ No newline at end of file diff --git a/app/lib/l10n/app_tr.arb b/app/lib/l10n/app_tr.arb index 18ed24bfee8a..45f3c34fba80 100644 --- a/app/lib/l10n/app_tr.arb +++ b/app/lib/l10n/app_tr.arb @@ -99,6 +99,7 @@ }, "background": "Arkaplan", "applyBackground": "Apply background", + "applyAreas": "Apply areas", "currentPage": "Current page", "allPages": "All pages", "box": "Kutu", @@ -123,6 +124,7 @@ }, "defaultPalette": "Varsayılan palet", "highlighter": "Fosforlu kalem", + "combinePaths": "Combine paths", "add": "Ekle", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Hold a key to temporarily switch to another tool. Releasing the key switches back to the previous tool.", "key": "Key", "bringMovedElementsToFront": "Bring moved elements to front", - "addTool": "Add tool" + "addTool": "Add tool", + "nextPage": "Next page", + "previousPage": "Previous page" } \ No newline at end of file diff --git a/app/lib/l10n/app_uk.arb b/app/lib/l10n/app_uk.arb index 676ff2974623..76e835adfbf9 100644 --- a/app/lib/l10n/app_uk.arb +++ b/app/lib/l10n/app_uk.arb @@ -99,6 +99,7 @@ }, "background": "Фон", "applyBackground": "Застосувати тло", + "applyAreas": "Застосувати області", "currentPage": "Поточна сторінка", "allPages": "Усі сторінки", "box": "Ящик", @@ -123,6 +124,7 @@ }, "defaultPalette": "Стандартна палітра", "highlighter": "Маркер", + "combinePaths": "Комбайнові шляхи", "add": "Додати", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Утримуйте клавішу, щоб тимчасово переключитися на інший інструмент. Звільнення перемикачів ключів знову до попереднього інструменту.", "key": "Ключ", "bringMovedElementsToFront": "Принести переміщені елементи на передній план", - "addTool": "Додати інструмент" + "addTool": "Додати інструмент", + "nextPage": "Наступна сторінка", + "previousPage": "Попередня сторінка" } \ No newline at end of file diff --git a/app/lib/l10n/app_vi.arb b/app/lib/l10n/app_vi.arb index 26be4116052b..93ddf4539c21 100644 --- a/app/lib/l10n/app_vi.arb +++ b/app/lib/l10n/app_vi.arb @@ -99,6 +99,7 @@ }, "background": "Nền", "applyBackground": "Apply background", + "applyAreas": "Apply areas", "currentPage": "Current page", "allPages": "All pages", "box": "Hộp", @@ -123,6 +124,7 @@ }, "defaultPalette": "Bảng màu mặc định", "highlighter": "Bút đánh dấu", + "combinePaths": "Combine paths", "add": "Thêm", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Hold a key to temporarily switch to another tool. Releasing the key switches back to the previous tool.", "key": "Key", "bringMovedElementsToFront": "Bring moved elements to front", - "addTool": "Add tool" + "addTool": "Add tool", + "nextPage": "Next page", + "previousPage": "Previous page" } \ No newline at end of file diff --git a/app/lib/l10n/app_zh-Hant.arb b/app/lib/l10n/app_zh-Hant.arb index d0484e75c6f5..879cb14c6f08 100644 --- a/app/lib/l10n/app_zh-Hant.arb +++ b/app/lib/l10n/app_zh-Hant.arb @@ -99,6 +99,7 @@ }, "background": "背景", "applyBackground": "Apply background", + "applyAreas": "Apply areas", "currentPage": "Current page", "allPages": "All pages", "box": "盒子", @@ -123,6 +124,7 @@ }, "defaultPalette": "預設調色板", "highlighter": "螢光筆", + "combinePaths": "Combine paths", "add": "新增", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "Hold a key to temporarily switch to another tool. Releasing the key switches back to the previous tool.", "key": "Key", "bringMovedElementsToFront": "Bring moved elements to front", - "addTool": "Add tool" + "addTool": "Add tool", + "nextPage": "Next page", + "previousPage": "Previous page" } \ No newline at end of file diff --git a/app/lib/l10n/app_zh.arb b/app/lib/l10n/app_zh.arb index fc531d1a5281..a75df6f1d6a7 100644 --- a/app/lib/l10n/app_zh.arb +++ b/app/lib/l10n/app_zh.arb @@ -99,6 +99,7 @@ }, "background": "背景", "applyBackground": "应用背景", + "applyAreas": "应用区域", "currentPage": "当前页面", "allPages": "所有页面", "box": "框", @@ -123,6 +124,7 @@ }, "defaultPalette": "默认调色板", "highlighter": "高亮器", + "combinePaths": "合并路径", "add": "添加", "@add": { "description": "Add action" @@ -1367,5 +1369,7 @@ "holdShortcutsDescription": "按住键暂时切换到另一个工具。释放键切换回上一个工具。", "key": "关键字", "bringMovedElementsToFront": "将移动元素带到前端", - "addTool": "添加工具" + "addTool": "添加工具", + "nextPage": "下一页", + "previousPage": "上一页" } \ No newline at end of file diff --git a/docs/src/content/docs/af/docs/v2/onenote.md b/docs/src/content/docs/af/docs/v2/onenote.md new file mode 100644 index 000000000000..fe876159e900 --- /dev/null +++ b/docs/src/content/docs/af/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Acknowledgements + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/ar/docs/v2/onenote.md b/docs/src/content/docs/ar/docs/v2/onenote.md new file mode 100644 index 000000000000..1fbab629edda --- /dev/null +++ b/docs/src/content/docs/ar/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## الإشادة + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/ca/docs/v2/onenote.md b/docs/src/content/docs/ca/docs/v2/onenote.md new file mode 100644 index 000000000000..fe876159e900 --- /dev/null +++ b/docs/src/content/docs/ca/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Acknowledgements + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/cs/docs/v2/onenote.md b/docs/src/content/docs/cs/docs/v2/onenote.md new file mode 100644 index 000000000000..f78b590b6577 --- /dev/null +++ b/docs/src/content/docs/cs/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Poděkování + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/da/docs/v2/onenote.md b/docs/src/content/docs/da/docs/v2/onenote.md new file mode 100644 index 000000000000..880ed21cd659 --- /dev/null +++ b/docs/src/content/docs/da/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Anerkendelser + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/de/docs/v2/onenote.md b/docs/src/content/docs/de/docs/v2/onenote.md new file mode 100644 index 000000000000..93c81b3da0c8 --- /dev/null +++ b/docs/src/content/docs/de/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Unterstützte Dateitypen + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Danksagungen + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/el/docs/v2/onenote.md b/docs/src/content/docs/el/docs/v2/onenote.md new file mode 100644 index 000000000000..155135e72d0a --- /dev/null +++ b/docs/src/content/docs/el/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Ευχαριστίες + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/es/docs/v2/onenote.md b/docs/src/content/docs/es/docs/v2/onenote.md new file mode 100644 index 000000000000..82166ea27a3d --- /dev/null +++ b/docs/src/content/docs/es/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Agradecimientos + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/fi/docs/v2/onenote.md b/docs/src/content/docs/fi/docs/v2/onenote.md new file mode 100644 index 000000000000..b50da971d27b --- /dev/null +++ b/docs/src/content/docs/fi/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Kiitokset + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/fr/docs/v2/onenote.md b/docs/src/content/docs/fr/docs/v2/onenote.md new file mode 100644 index 000000000000..e6d74729294a --- /dev/null +++ b/docs/src/content/docs/fr/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Remerciements + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/he/docs/v2/onenote.md b/docs/src/content/docs/he/docs/v2/onenote.md new file mode 100644 index 000000000000..fe876159e900 --- /dev/null +++ b/docs/src/content/docs/he/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Acknowledgements + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/hi/docs/v2/onenote.md b/docs/src/content/docs/hi/docs/v2/onenote.md new file mode 100644 index 000000000000..fe876159e900 --- /dev/null +++ b/docs/src/content/docs/hi/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Acknowledgements + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/hu/docs/v2/onenote.md b/docs/src/content/docs/hu/docs/v2/onenote.md new file mode 100644 index 000000000000..fe876159e900 --- /dev/null +++ b/docs/src/content/docs/hu/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Acknowledgements + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/id/docs/v2/onenote.md b/docs/src/content/docs/id/docs/v2/onenote.md new file mode 100644 index 000000000000..fe876159e900 --- /dev/null +++ b/docs/src/content/docs/id/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Acknowledgements + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/it/docs/v2/onenote.md b/docs/src/content/docs/it/docs/v2/onenote.md new file mode 100644 index 000000000000..153c2b099fd7 --- /dev/null +++ b/docs/src/content/docs/it/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Riconoscimenti + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/ja/docs/v2/onenote.md b/docs/src/content/docs/ja/docs/v2/onenote.md new file mode 100644 index 000000000000..02eb79447dcd --- /dev/null +++ b/docs/src/content/docs/ja/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## 謝辞 + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/ko/docs/v2/onenote.md b/docs/src/content/docs/ko/docs/v2/onenote.md new file mode 100644 index 000000000000..fe876159e900 --- /dev/null +++ b/docs/src/content/docs/ko/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Acknowledgements + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/nl/docs/v2/onenote.md b/docs/src/content/docs/nl/docs/v2/onenote.md new file mode 100644 index 000000000000..fe876159e900 --- /dev/null +++ b/docs/src/content/docs/nl/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Acknowledgements + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/no/docs/v2/onenote.md b/docs/src/content/docs/no/docs/v2/onenote.md new file mode 100644 index 000000000000..841b645a8210 --- /dev/null +++ b/docs/src/content/docs/no/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Anerkjennelser + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/or/docs/v2/onenote.md b/docs/src/content/docs/or/docs/v2/onenote.md new file mode 100644 index 000000000000..fe876159e900 --- /dev/null +++ b/docs/src/content/docs/or/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Acknowledgements + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/pl/docs/v2/onenote.md b/docs/src/content/docs/pl/docs/v2/onenote.md new file mode 100644 index 000000000000..4c64a1af9ee3 --- /dev/null +++ b/docs/src/content/docs/pl/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Potwierdzenia + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/pt-br/docs/v2/onenote.md b/docs/src/content/docs/pt-br/docs/v2/onenote.md new file mode 100644 index 000000000000..d3673b3da305 --- /dev/null +++ b/docs/src/content/docs/pt-br/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Reconhecimentos + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/pt/docs/v2/onenote.md b/docs/src/content/docs/pt/docs/v2/onenote.md new file mode 100644 index 000000000000..d3673b3da305 --- /dev/null +++ b/docs/src/content/docs/pt/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Reconhecimentos + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/ro/docs/v2/onenote.md b/docs/src/content/docs/ro/docs/v2/onenote.md new file mode 100644 index 000000000000..33c0dd134b28 --- /dev/null +++ b/docs/src/content/docs/ro/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Mulţumiri + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/ru/docs/v2/onenote.md b/docs/src/content/docs/ru/docs/v2/onenote.md new file mode 100644 index 000000000000..d2afa28534d1 --- /dev/null +++ b/docs/src/content/docs/ru/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Выражение признательности + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/sr/docs/v2/onenote.md b/docs/src/content/docs/sr/docs/v2/onenote.md new file mode 100644 index 000000000000..fe876159e900 --- /dev/null +++ b/docs/src/content/docs/sr/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Acknowledgements + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/sv/docs/v2/onenote.md b/docs/src/content/docs/sv/docs/v2/onenote.md new file mode 100644 index 000000000000..cf4d33a2295e --- /dev/null +++ b/docs/src/content/docs/sv/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Erkännanden + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/th/docs/v2/onenote.md b/docs/src/content/docs/th/docs/v2/onenote.md new file mode 100644 index 000000000000..fe876159e900 --- /dev/null +++ b/docs/src/content/docs/th/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Acknowledgements + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/tr/docs/v2/onenote.md b/docs/src/content/docs/tr/docs/v2/onenote.md new file mode 100644 index 000000000000..6c2f15fd7b41 --- /dev/null +++ b/docs/src/content/docs/tr/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Teşekkürler + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/uk/docs/v2/onenote.md b/docs/src/content/docs/uk/docs/v2/onenote.md new file mode 100644 index 000000000000..4345f6831e16 --- /dev/null +++ b/docs/src/content/docs/uk/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Подяки + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/vi/docs/v2/onenote.md b/docs/src/content/docs/vi/docs/v2/onenote.md new file mode 100644 index 000000000000..fe876159e900 --- /dev/null +++ b/docs/src/content/docs/vi/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Acknowledgements + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/zh-hant/docs/v2/onenote.md b/docs/src/content/docs/zh-hant/docs/v2/onenote.md new file mode 100644 index 000000000000..fe876159e900 --- /dev/null +++ b/docs/src/content/docs/zh-hant/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## Acknowledgements + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. diff --git a/docs/src/content/docs/zh/docs/v2/onenote.md b/docs/src/content/docs/zh/docs/v2/onenote.md new file mode 100644 index 000000000000..4ee152e92c17 --- /dev/null +++ b/docs/src/content/docs/zh/docs/v2/onenote.md @@ -0,0 +1,297 @@ +--- +title: Importing from Microsoft OneNote +--- + +:::note +This feature is only available in Butterfly 2.6.0-beta.1 and later. +::: + +Butterfly can convert Microsoft OneNote sections and packaged notebooks into +Butterfly documents. + +## Supported file types + +Butterfly supports these OneNote formats: + +- `.one` — a single OneNote section +- `.onepkg` — a packaged OneNote notebook containing multiple sections and + section groups + +For transferring an entire notebook, `.onepkg` is usually the most convenient +format. Use `.one` when you only want to transfer one section. + +Butterfly does not currently import loose `.onetoc2` files or a ZIP file +downloaded from OneNote for the web directly. + +:::note +Keep the original OneNote files until you have reviewed the imported document. +Importing is a conversion, and some OneNote features cannot be represented +exactly in Butterfly. +::: + +## Exporting from the OneNote desktop app + +The instructions below apply to the OneNote desktop application on Windows, +as included with Microsoft 365 and recent desktop versions of Microsoft +Office. + +Before exporting, allow OneNote to finish synchronizing the notebook. This is +especially important for notebooks stored in OneDrive or SharePoint. + +### Exporting an entire notebook + +1. Open the notebook in the OneNote desktop application. +2. Select **File** in the upper-left corner. +3. Select **Export**. +4. Under **Export Current**, select **Notebook**. +5. Under **Select Format**, select **OneNote Package (`*.onepkg`)**. +6. Select **Export**. +7. Choose a local folder and enter a name for the exported file. +8. Select **Save**. + +You can now import the resulting `.onepkg` file into Butterfly. + +The package contains the notebook's sections and section groups. Butterfly +uses this structure when naming and organizing the imported pages. + +:::note +The available formats can differ between OneNote versions. If **OneNote +Package (`*.onepkg`)** is not shown, export the sections separately as `.one` +files using the instructions below. +::: + +### Exporting a single section + +1. Open the notebook in the OneNote desktop application. + +2. Select the section that you want to export. + +3. Select **File**. + +4. Select **Export**. + +5. Under **Export Current**, select **Section**. + +6. Under **Select Format**, select **OneNote Section (`*.one`)**. + + Depending on your OneNote version, this option may be named + **OneNote 2010–2016 Section (`*.one`)**. + +7. Select **Export**. + +8. Choose a local folder and enter a name for the section. + +9. Select **Save**. + +Repeat these steps for every section that you want to move to Butterfly. + +Exporting sections individually does not preserve the complete notebook +hierarchy in one file. Each `.one` file is imported separately. + +### Do not export as PDF for a OneNote import + +The OneNote export screen also offers formats such as PDF and XPS. These +formats contain a rendered copy of the pages rather than their original +OneNote structure. + +Choose `.one` or `.onepkg` when you want Butterfly to convert editable content +such as text, handwriting, images, tables, and attachments. + +Use PDF import only when a visual, non-editable representation of the OneNote +pages is sufficient. + +## Alternative: downloading a notebook from OneNote for the web + +Microsoft also provides a notebook download through OneNote for the web. + +This method is currently limited to notebooks stored in a personal OneDrive +account. It is not available for notebooks stored in a work or school +OneDrive account or in SharePoint. + +1. Open OneNote for the web in a browser. +2. Find the notebook in the notebook list. +3. Right-click the notebook. +4. Select **Export notebook**. +5. Confirm the export. +6. Wait for the notebook to be downloaded. +7. Extract the downloaded ZIP archive. + +The extracted notebook usually contains individual `.one` section files and +an `.onetoc2` table-of-contents file. + +Butterfly cannot currently import the downloaded ZIP archive or the +`.onetoc2` file directly. Import the contained `.one` section files one at a +time. + +For a complete notebook import that preserves more of its hierarchy, prefer a +`.onepkg` export from the Windows desktop application when that option is +available. + +## Importing the file into Butterfly + +1. Open Butterfly. +2. Open an existing Butterfly document or create a new one. +3. Open the **Add** dialog. +4. Select **OneNote**. +5. Choose the exported `.one` or `.onepkg` file. +6. Wait while Butterfly reads and converts the file. +7. Complete any dialogs concerning embedded XPS printouts. +8. Review the imported pages. + +A `.one` file imports one section. A `.onepkg` file can contain multiple +sections and section groups. + +Butterfly creates a page for each supported OneNote page and attempts to +preserve supported content, including: + +- Text and basic rich-text formatting +- Handwriting and ink strokes +- Images +- Tables +- Attached files +- Printed document pages, when their XPS data can be converted to PDF + +Section and section-group names are used as part of the imported Butterfly +page paths. + +## Import limitations + +OneNote and Butterfly use different document models. The importer therefore +cannot guarantee a pixel-perfect or fully editable copy of every page. + +After importing, check in particular: + +- Text positioning and wrapping +- Fonts that are not installed on the current device +- Complex formatting +- Tables and nested content +- Equations and uncommon OneNote objects +- Embedded files +- Page backgrounds +- Printed documents +- Internal links between OneNote pages +- Password-protected or encrypted sections + +Unsupported or unknown OneNote objects may be omitted. Parser warnings may +also be stored with the imported document or page. + +For large notebooks, importing can take some time. Notebooks containing many +images, attachments, or printed documents also require more memory and disk +space. + +## Printed documents and XPS files + +OneNote may store inserted file printouts as XPS files. Butterfly displays +these printouts as PDF elements, so the XPS data must first be converted to +PDF. + +### Automatic conversion + +On native desktop platforms, Butterfly first tries to run the following +command: + +```text +xpstopdf +``` + +Automatic conversion only works when `xpstopdf` is installed and available +through the system's `PATH`. + +When the command is missing or conversion fails, Butterfly opens the manual +conversion workflow. + +Automatic XPS conversion is unavailable in the web version of Butterfly. + +### Manual conversion + +When Butterfly asks you to convert an XPS printout manually: + +1. Select **Convert manually**. +2. Select **Export XPS**. +3. Save the exported `.xps` file somewhere you can find it. +4. Convert the XPS file to PDF using an external application. +5. Return to Butterfly. +6. Select **Select converted PDF**. +7. Select the corresponding PDF file. + +Butterfly verifies that the selected file appears to be a valid PDF before +continuing the import. + +Canceling the file picker returns to the conversion dialog and does not cancel +the complete OneNote import. + +You can also choose: + +- **Skip this file** — omit only the current printout +- **Skip all XPS files** — omit all remaining XPS printouts while importing + the rest of the notebook +- **Export XPS again** — save another copy of the current XPS file + +Skipping an XPS printout does not skip the OneNote page or cancel the remaining +notebook import. Only the affected printed document is omitted. + +:::caution[MuPDF and OneNote XPS files] +MuPDF cannot handle the XPS files produced or embedded by OneNote correctly. + +Do not use MuPDF-based tools, including `mutool`, to convert these files. +Use a different XPS-to-PDF converter. +::: + +When several OneNote printout pages refer to the same XPS document, Butterfly +reuses the converted PDF instead of asking for the same conversion repeatedly. + +## Troubleshooting + +### The OneNote option is missing in Butterfly + +Make sure you are using a Butterfly version that includes OneNote import +support. The importer accepts files ending in `.one` and `.onepkg`. + +### OneNote does not offer a `.onepkg` format + +The available export formats depend on the OneNote edition and account +configuration. + +Export each section as a `.one` file instead. For personal OneDrive notebooks, +you can also download the notebook through OneNote for the web, extract the +archive, and import its `.one` files separately. + +### Butterfly asks for `xpstopdf` + +The notebook contains one or more printed documents stored as XPS. + +Install `xpstopdf` and ensure that it is available through `PATH`, or use the +manual conversion workflow. + +### The converted PDF is rejected + +Make sure you selected the actual converted PDF rather than the original XPS +file or a renamed file. + +Changing a filename from `.xps` to `.pdf` does not convert the document. + +### A printout is missing after import + +The XPS conversion may have failed or the file may have been skipped. Import +the notebook again and provide a converted PDF when Butterfly displays the +conversion dialog. + +### Some content is missing or formatted differently + +The object may not yet be supported by the OneNote parser or by Butterfly's +document model. Keep the original OneNote notebook and compare it with the +imported result. + +## 致谢 + +Butterfly's OneNote import uses Dart and Flutter bindings based on +[onenote.rs](https://github.com/msiemens/onenote.rs), an open-source Microsoft +OneNote file parser implemented in Rust. + +Special thanks to Matthias Siemens and all contributors to `onenote.rs` for +their work on understanding and implementing the OneNote file formats. +Without their project, native OneNote file import in Butterfly would not be +possible. + +`onenote.rs` is an independent open-source project and is not affiliated with +or endorsed by Microsoft. From 55729e2cb1f00d70fb7aa36e9046107b63743342 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 6 Jul 2026 11:50:55 +0200 Subject: [PATCH 028/117] Fix polygon disappears --- app/lib/handlers/polygon.dart | 7 ++- app/test/handlers/polygon_handler_test.dart | 61 ++++++++++++++++++++- metadata/en-US/changelogs/187.txt | 1 + 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/app/lib/handlers/polygon.dart b/app/lib/handlers/polygon.dart index 31c488712853..732fc2f8e847 100644 --- a/app/lib/handlers/polygon.dart +++ b/app/lib/handlers/polygon.dart @@ -119,8 +119,9 @@ class PolygonHandler extends Handler with ColoredHandler { final element = _element; if (element != null) { _element = element.copyWith(property: tool.property); - bloc.refreshForegrounds(); - bloc.refreshToolbar(); + unawaited(bloc.currentIndexCubit.refreshToolbar(bloc)); + unawaited(bloc.refreshForegrounds()); + return; } changeTool(bloc, tool); } @@ -382,7 +383,7 @@ class PolygonHandler extends Handler with ColoredHandler { _selectedPointIndex = null; _dragTarget = _PolygonDragTarget.newHandle; - await bloc.refreshForegrounds(); + unawaited(bloc.refreshForegrounds()); _submitElement(bloc); } diff --git a/app/test/handlers/polygon_handler_test.dart b/app/test/handlers/polygon_handler_test.dart index 50a829892f48..d534ec15de90 100644 --- a/app/test/handlers/polygon_handler_test.dart +++ b/app/test/handlers/polygon_handler_test.dart @@ -5,6 +5,7 @@ import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/handlers/handler.dart'; import 'package:butterfly/models/viewport.dart'; +import 'package:butterfly/renderers/renderer.dart'; import 'package:butterfly/views/toolbar/polygon.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -93,11 +94,69 @@ void main() { final toolbar = handler.getToolbar(bloc!) as PolygonToolbarView; toolbar.onToolChanged(toolbar.tool.copyWith(property: updatedProperty)); await _settleBlocEvents(); + final updatedState = bloc!.stream + .where((state) => state is DocumentLoadSuccess) + .cast() + .firstWhere( + (state) => + (state.page.content.single as PolygonElement).property == + updatedProperty, + ); (handler.getToolbar(bloc!) as PolygonToolbarView).onSubmit?.call(); - await _settleBlocEvents(); + await updatedState; final state = bloc!.state as DocumentLoadSuccess; final updatedElement = state.page.content.single as PolygonElement; expect(updatedElement.property, updatedProperty); }); + + test('toolbar changes keep edited polygon visible in foregrounds', () async { + const originalProperty = PolygonProperty( + strokeWidth: 3, + paint: ElementPaint.solid(color: SRGBColor(0xFF000000)), + ); + const updatedProperty = PolygonProperty( + strokeWidth: 9, + paint: ElementPaint.solid(color: SRGBColor(0xFFFF0000)), + ); + final element = PolygonElement( + id: 'polygon', + points: const [PolygonPoint(0, 0), PolygonPoint(10, 10)], + property: originalProperty, + ); + final page = DocumentPage( + layers: [ + DocumentLayer(id: 'layer', content: [element]), + ], + ); + final (data, pageName) = NoteData(Archive()).setPage(page, 'Page 1'); + bloc = DocumentBloc( + fileSystem, + currentIndexCubit, + windowCubit, + data, + const AssetLocation(path: 'test-note.bfly'), + null, + page, + pageName, + ); + final handler = PolygonHandler( + PolygonTool(id: 'polygon-tool', property: originalProperty), + )..editElement(element); + final toolbar = handler.getToolbar(bloc!) as PolygonToolbarView; + toolbar.onToolChanged(toolbar.tool.copyWith(property: updatedProperty)); + await _settleBlocEvents(); + + final polygon = handler + .createForegrounds( + currentIndexCubit, + (bloc!.state as DocumentLoadSuccess).data, + (bloc!.state as DocumentLoadSuccess).page, + (bloc!.state as DocumentLoadSuccess).info, + ) + .whereType() + .single + .element; + expect(polygon.property, updatedProperty); + }); } diff --git a/metadata/en-US/changelogs/187.txt b/metadata/en-US/changelogs/187.txt index 30157d550877..5a074d007ce6 100644 --- a/metadata/en-US/changelogs/187.txt +++ b/metadata/en-US/changelogs/187.txt @@ -14,5 +14,6 @@ * Fix saved documents being saved again * Fix imported documents starting as saved * Fix zoom slider and reset button not working if zoom is locked +* Fix polygon disappears Read more here: https://linwood.dev/butterfly/2.6.0-beta.1 \ No newline at end of file From a35216a8d84cd824e02c2f9bfd37680ed9ef56b9 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 6 Jul 2026 11:55:12 +0200 Subject: [PATCH 029/117] Upgrade dependencies --- .github/workflows/build.yml | 2 +- .github/workflows/deploy.yml | 2 +- SECURITY.md | 2 +- app/android/Gemfile.lock | 13 +- app/pubspec.lock | 36 +- app/rust-toolchain.toml | 2 +- docs/package.json | 12 +- docs/pnpm-lock.yaml | 1167 +++++++++++++++------------------- 8 files changed, 543 insertions(+), 693 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 736822b8985e..8bb3a227d4d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -460,7 +460,7 @@ jobs: - name: Setup node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 26 - name: Install appdmg run: | python3 -m pip install setuptools diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 42c6c4f816c4..d4d29abacde0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -22,7 +22,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 26 cache: "pnpm" cache-dependency-path: docs/pnpm-lock.yaml - name: Install dependencies diff --git a/SECURITY.md b/SECURITY.md index 1daa655498a3..1148af13e8f6 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ | Version | Supported | | | -------------------------- | ------------------ | ----------------------------------------------------------------------------- | -| 2.6-dev (Dreamy Duskywing) | :warning: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.6.0-beta.0) | +| 2.6-dev (Dreamy Duskywing) | :warning: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.6.0-beta.1) | | 2.5.3 (Crimson Red) | :white_check_mark: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.5.3) | | 2.4.4 (Black Hairstreak) | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.4.4) | | 2.3.4 (Adonis Blue) | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.3.4) | diff --git a/app/android/Gemfile.lock b/app/android/Gemfile.lock index f3cafe246c4d..dbcd9c66fba3 100644 --- a/app/android/Gemfile.lock +++ b/app/android/Gemfile.lock @@ -8,7 +8,7 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1262.0) + aws-partitions (1.1265.0) aws-sdk-core (3.252.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -42,7 +42,8 @@ GEM domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.112.0) + excon (1.5.0) + logger faraday (1.10.6) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -72,10 +73,10 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.4.1) - fastlane (2.236.1) + fastlane (2.237.0) CFPropertyList (>= 2.3, < 5.0.0) abbrev (~> 0.1) - addressable (>= 2.8, < 3.0.0) + addressable (>= 2.9.0, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.197) babosa (>= 1.0.3, < 2.0.0) @@ -87,7 +88,7 @@ GEM csv (~> 3.3) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) - excon (>= 0.71.0, < 1.0.0) + excon (>= 0.71.0, < 2.0.0) faraday (~> 1.0) faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) @@ -148,7 +149,7 @@ GEM base64 (~> 0.2) faraday (>= 1.0, < 3.a) google-cloud-errors (1.6.0) - google-cloud-storage (1.61.0) + google-cloud-storage (1.62.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-core (>= 0.18, < 2) diff --git a/app/pubspec.lock b/app/pubspec.lock index 089117761427..112c99b58d66 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -164,10 +164,10 @@ packages: dependency: transitive description: name: camera_android_camerax - sha256: e20c1e92ce6797d9ae9b1db1e09a4c1039a04827d0b24985f5da3840b96948ac + sha256: "50c3fd81228826635af4875330febb193c74a2e12a4d19c1b95e2a67f0017bb3" url: "https://pub.dev" source: hosted - version: "0.7.2+1" + version: "0.7.3" camera_avfoundation: dependency: transitive description: @@ -284,10 +284,10 @@ packages: dependency: "direct main" description: name: cross_file - sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" url: "https://pub.dev" source: hosted - version: "0.3.5+2" + version: "0.3.5+4" crypto: dependency: transitive description: @@ -667,10 +667,10 @@ packages: dependency: "direct main" description: name: idb_shim - sha256: "2c81d22578b71951004f85ad3cf0eca23aeea1e18b74cf5efce2a267dce17579" + sha256: dcf59807be0cdf39f305c997b7b29cb06fdef26311559934c82cbc3e8c241f1c url: "https://pub.dev" source: hosted - version: "2.9.5" + version: "2.9.6+1" image: dependency: "direct main" description: @@ -1226,42 +1226,42 @@ packages: dependency: transitive description: name: screen_retriever - sha256: "42cc3b402a0f67d2455a0d067553d0f13453f6a008d98eababf8b63958d506bd" + sha256: ace919117a7520c13a50a6259e60c4a0d4cbe98809468792a91b5c5adada2aa6 url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.2" screen_retriever_linux: dependency: transitive description: name: screen_retriever_linux - sha256: "2a476f1a5538065bc5badf376cfdc83d6ecf07d77eb2391b9c2bff5a76970048" + sha256: "7b52006a5ceae1f3d5af7f77188c3290d6e7d8ded16d99809bea84967c65c257" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.2" screen_retriever_macos: dependency: transitive description: name: screen_retriever_macos - sha256: b5abb900fcb86614ff10b738b34e37b9e1d03b0447280668e2bc8a98bdc7bd59 + sha256: a1489b99cce597c45a54b9aae1cd94c8d4705353b7e0bb2457a6e4de44e0ad8a url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.2" screen_retriever_platform_interface: dependency: transitive description: name: screen_retriever_platform_interface - sha256: "3af22d926bedf20c2caa308eea376776451a3af125919ce072e56525fded8901" + sha256: "94a5535277510a63184ca178ce12a1449bc0b38618879aa1c18bf57369c5064a" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.2" screen_retriever_windows: dependency: transitive description: name: screen_retriever_windows - sha256: c44b38a4c4bab34af259180a70a4eee1e29384e7b82e627c9faa68afcdab2e73 + sha256: dafc6922b0bfbf1d48cf3ccbf519b4fff47bdcb820da1728ea6db675fecc9324 url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.2" sembast: dependency: transitive description: @@ -1688,10 +1688,10 @@ packages: dependency: "direct main" description: name: window_manager - sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd" + sha256: "05c231fd7b23d2380f14c5cc10b7b93d60d4fa4a2fb4e0f032de27e44b5560e9" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.5.2" xdg_directories: dependency: transitive description: diff --git a/app/rust-toolchain.toml b/app/rust-toolchain.toml index 0f87b4480331..2d45363a5be0 100644 --- a/app/rust-toolchain.toml +++ b/app/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.96.0" +channel = "1.96.1" diff --git a/docs/package.json b/docs/package.json index 20732e295172..bdd3e655b5b7 100644 --- a/docs/package.json +++ b/docs/package.json @@ -11,24 +11,24 @@ }, "dependencies": { "@astrojs/check": "^0.9.9", - "@astrojs/markdown-satteri": "^0.3.2", - "@astrojs/react": "^6.0.0", - "@astrojs/starlight": "^0.41.1", + "@astrojs/markdown-satteri": "^0.3.3", + "@astrojs/react": "^6.0.1", + "@astrojs/starlight": "^0.41.3", "@linwooddev/style": "github:LinwoodDev/style#efbdf8f05d4ef1cf6fec65c90194d30b68b3c64e&path:/packages/web", "@phosphor-icons/react": "^2.1.10", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", - "astro": "^7.0.3", + "astro": "^7.0.6", "katex": "^0.17.0", "react": "^19.2.7", "react-dom": "^19.2.7", "typescript": "^6.0.3" }, - "packageManager": "pnpm@11.9.0", + "packageManager": "pnpm@11.10.0", "devDependencies": { "@vite-pwa/astro": "^1.2.0", "sass": "^1.101.0", - "sharp": "^0.35.2", + "sharp": "^0.35.3", "vite-plugin-pwa": "^1.3.0", "workbox-window": "^7.4.1" } diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index 78e9dd705cd0..eae677d30882 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -10,16 +10,16 @@ importers: dependencies: '@astrojs/check': specifier: ^0.9.9 - version: 0.9.9(prettier@3.9.3)(typescript@6.0.3) + version: 0.9.9(prettier@3.9.4)(typescript@6.0.3) '@astrojs/markdown-satteri': - specifier: ^0.3.2 - version: 0.3.2 + specifier: ^0.3.3 + version: 0.3.3 '@astrojs/react': - specifier: ^6.0.0 - version: 6.0.0(@types/node@24.13.2)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + specifier: ^6.0.1 + version: 6.0.1(@types/node@26.1.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) '@astrojs/starlight': - specifier: ^0.41.1 - version: 0.41.1(@astrojs/markdown-remark@7.2.0)(astro@7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3) + specifier: ^0.41.3 + version: 0.41.3(@astrojs/markdown-remark@7.2.1)(astro@7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3) '@linwooddev/style': specifier: github:LinwoodDev/style#efbdf8f05d4ef1cf6fec65c90194d30b68b3c64e&path:/packages/web version: https://codeload.github.com/LinwoodDev/style/tar.gz/efbdf8f05d4ef1cf6fec65c90194d30b68b3c64e#path:/packages/web @@ -33,8 +33,8 @@ importers: specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.17) astro: - specifier: ^7.0.3 - version: 7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + specifier: ^7.0.6 + version: 7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) katex: specifier: ^0.17.0 version: 0.17.0 @@ -50,16 +50,16 @@ importers: devDependencies: '@vite-pwa/astro': specifier: ^1.2.0 - version: 1.2.0(astro@7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1)) + version: 1.2.0(astro@7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1)) sass: specifier: ^1.101.0 version: 1.101.0 sharp: - specifier: ^0.35.2 - version: 0.35.2 + specifier: ^0.35.3 + version: 0.35.3(@types/node@26.1.0) vite-plugin-pwa: specifier: ^1.3.0 - version: 1.3.0(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1) + version: 1.3.0(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1) workbox-window: specifier: ^7.4.1 version: 7.4.1 @@ -78,78 +78,79 @@ packages: peerDependencies: typescript: ^5.0.0 || ^6.0.0 - '@astrojs/compiler-binding-darwin-arm64@0.2.3': - resolution: {integrity: sha512-sJIHeL1ONXEBLob8ZaXfmX6iCftUno08G/cMXj2FJnL0xNbHuELcEq1mjxHVFHNgUYu4P7xJNm2mpc0zUEPoKw==} + '@astrojs/compiler-binding-darwin-arm64@0.3.0': + resolution: {integrity: sha512-3n0uu+uJpnCq8b4JFi3uGDsIisAvHctxSmH+cIO9Gbei1H1Y1QXaYboXyiWJugUmprr3OEYP7+LdodzpVFzLMQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@astrojs/compiler-binding-darwin-x64@0.2.3': - resolution: {integrity: sha512-P0NYu6aaIeLCqFfszxxBHL0a5WRaYigNVbDoO654Gi5Q2au5duDb5xZBv5EqUg4qnQVC173FXNvGZu1M7nk+/w==} + '@astrojs/compiler-binding-darwin-x64@0.3.0': + resolution: {integrity: sha512-scxNGKjOBydMo1QR4LtK0FMgh7ubQomJDv953nz2msQFkPKke/0FpPv/cQM0T/kuZdReZQFU8Oz3iOrP/6WHEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@astrojs/compiler-binding-linux-arm64-gnu@0.2.3': - resolution: {integrity: sha512-PqVN5AqhuDqfx3ejaerwrC8codpV9jnyKV+IOel027qsJ1anFUJLdjUlY8VVys0xgd8lmqveX11OkcaQj/otTg==} + '@astrojs/compiler-binding-linux-arm64-gnu@0.3.0': + resolution: {integrity: sha512-NZrWLolVUANmrnl0zrFK/Sx5Sock1gEUT49ALfMTTCA5Ya2ec/BoJXMIg4KgE+wZcrdXJ8e+WyEhM7YLk/FJkA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@astrojs/compiler-binding-linux-arm64-musl@0.2.3': - resolution: {integrity: sha512-O3e2CbN4yTsRguWYNnRd0p5YQ0H3fb7KpcR0W4R319q/gq5B1pJ7eqNbiO3b8g2AuiEcRTiUz5jeGT9j69cxOQ==} + '@astrojs/compiler-binding-linux-arm64-musl@0.3.0': + resolution: {integrity: sha512-PjwRmKgMFDsFhg82g0poXlIY8Qn3fMA3hXjaR0coJWJzTJsRH9ATU0j2ocigjtU1h3vL/yR7yLUxGj/lTCq73g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@astrojs/compiler-binding-linux-x64-gnu@0.2.3': - resolution: {integrity: sha512-hbLBjXVp+96psMe7/7uqyrquGiULXANrq6REVxxPK/I5VzebZ7LHmSfykmByUbLyR1u+K6CTBKgvdQsK2L+2Xw==} + '@astrojs/compiler-binding-linux-x64-gnu@0.3.0': + resolution: {integrity: sha512-Dr69VJYlnSfyL8gzELW6S4mE41P7TDPn1IKjwMnjdZ7+dxgJI50oMLFSk1LVe26bHmWB3ktuh8fDVK1THI9e9A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@astrojs/compiler-binding-linux-x64-musl@0.2.3': - resolution: {integrity: sha512-vIiEvOwrJfHZMaTmqUCrFTIwMYL0+PD3Rvy7kFDQgERyx3zhaw8CPa01MCCqa+/sj344BGrXKZ6ti37SgNLMhw==} + '@astrojs/compiler-binding-linux-x64-musl@0.3.0': + resolution: {integrity: sha512-AEt+bRw8PfImCcyRH1lpXVB8CdmQ1K/wPo5u99iec4/U/XdNvQZ715YVuNzIJpbJXelgQeZ5H2+Ea7XwRyWY5g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@astrojs/compiler-binding-wasm32-wasi@0.2.3': - resolution: {integrity: sha512-p9S2X8z/mUR2SMzAVJRFMCt8YaalKR+pjl2DgpdjzCQc6ww4bo8kiy54tgKqxZeNF5c+/2tCDTQIxVSm9V1FsA==} + '@astrojs/compiler-binding-wasm32-wasi@0.3.0': + resolution: {integrity: sha512-U80tA1j8V6LjhiTZzVCtG4E8hrNVVNXDGV5fCgJ94q8FU9CPH+XwdDDhLzBybfWhKfyItXmQiZNRPTiPCYTpVg==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@astrojs/compiler-binding-win32-arm64-msvc@0.2.3': - resolution: {integrity: sha512-vcCG6JttIb5vbSmcxO2O398hpVj7lQ349iS7cjgYP6ZuLVEnw+9qPAr2MM2kJkU5wEGZqJ2gyi/M7UJoPwH1iQ==} + '@astrojs/compiler-binding-win32-arm64-msvc@0.3.0': + resolution: {integrity: sha512-CpY1RII2r1XMpOUVD1VR/F2wtuRsiOCkFULS10Khyj8/DFZMtxVuUCAWGw+CW2Ka0h6eP3Xc1CA+glFlvXMPxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@astrojs/compiler-binding-win32-x64-msvc@0.2.3': - resolution: {integrity: sha512-hKssjNvC36e00Inb1GW1JsVyCFSCGnIjKem4S8q0VIW6cpWAUpvYB4qQU2HIDGD6SDX0ork4F5sWkNWkp2hrGQ==} + '@astrojs/compiler-binding-win32-x64-msvc@0.3.0': + resolution: {integrity: sha512-qmFbs769oeeGrRebAnCW7aBk8m71vf85W/dX/jddfx5Z06/w0wf7TZCfJPOX1Fld2t+4N+iXzfGEJG+zJQ+bzg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@astrojs/compiler-binding@0.2.3': - resolution: {integrity: sha512-Xz3iBNse+hXXD25IXxsuXEt2ai8klAWE15CRm/EQBc9+aE3jXaF07DZx+iakk3HC6NHvWlEPzLPyxsLgPzOJsw==} + '@astrojs/compiler-binding@0.3.0': + resolution: {integrity: sha512-zlsOT5COD9hRwplJCgQhS21unxON5AKirf0vgt1ijXwuseYIaZdm2ZOpF8fsz+DY9EyXx+I/ukxtg7uoBep68A==} engines: {node: ^20.19.0 || >=22.12.0} - '@astrojs/compiler-rs@0.2.3': - resolution: {integrity: sha512-JRAtRcPxS4JeAZEIQFQ6GecBs/Wyp4m6/E8vBNxSgVfo1AtRVLUqRCl5oCGOZ0X/BSBB3Vef/7IlzyiGKi2ORA==} + '@astrojs/compiler-rs@0.3.0': + resolution: {integrity: sha512-J2qEVHtIDjEM9TxwmwuebOGmZNwhKu/dR7P7qBpnJKGmBBX0vdweQ/4cEXhj8fBbWVUB5V12xWChri3CgKNULQ==} + engines: {node: '>=22.12.0'} '@astrojs/compiler@2.13.1': resolution: {integrity: sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg==} - '@astrojs/internal-helpers@0.10.0': - resolution: {integrity: sha512-Ry2R3VPeIN4uPCSA4xQc+e+vsJXkalKpEbDc07hV+a/o5Bs2N/s/uDcPJH/05L19DKh9tAy7e6JM3YZ6Cxfezw==} + '@astrojs/internal-helpers@0.10.1': + resolution: {integrity: sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q==} - '@astrojs/language-server@2.16.10': - resolution: {integrity: sha512-87VQ/5GSdHlRnUA+hGuerYyIGAj+9RbZmATyuKLEUePinUXhQ5YkRnRrHhOD9sSi5JOErLjrLkHnfZFEvGrV8w==} + '@astrojs/language-server@2.16.11': + resolution: {integrity: sha512-sJ/EfnFp0+gurTrkvONtd9qRqmMZLT9bHelfI1SA35CaQVTrRrA74qteOcNT/al1b9Atg3IiH1Jk/qfckyC+fg==} hasBin: true peerDependencies: prettier: ^3.0.0 @@ -160,18 +161,18 @@ packages: prettier-plugin-astro: optional: true - '@astrojs/markdown-remark@7.2.0': - resolution: {integrity: sha512-+YxmVQu1Bd+MFfSzjq1rOJvD9+nIOJzz5YIIhdIH01RrxRkKbyKoEgyIqP3yv51MhzMDgd79QaPv+kCVPT8vHw==} + '@astrojs/markdown-remark@7.2.1': + resolution: {integrity: sha512-jPVNIqTvk+yKviikszv/Y1U4jGUSKpp/Nw48QZV4qjWgp70j4Lkq3lhSDRbWwCfgKvEyO9GHuVbV1dM2WYXy1w==} - '@astrojs/markdown-satteri@0.3.2': - resolution: {integrity: sha512-feXuUPy41gVfeM7EHT1ciUim8ozGr+YHXab9uUBc1Hk8y60DQosO8ldL+AoPXnCAoGj1OChwHfvXmmJ6XVnY9A==} + '@astrojs/markdown-satteri@0.3.3': + resolution: {integrity: sha512-Lje33Ittd8UQGgbIIWQvhPkj5X5c4b1sZnZWX3JQV/AWpfbuQGxVi2ONt6+ScydcwfR4egilslEWyczMclrJ1g==} - '@astrojs/mdx@7.0.0': - resolution: {integrity: sha512-LKwNA8nnLtEM0auoP6OfH/UnlKe1Ub59qZjbcYkZjPBGw6PkJewWkA/1qwLpECvV6gMDd6TR6eqV9p/VYZrcrQ==} + '@astrojs/mdx@7.0.2': + resolution: {integrity: sha512-l+sJY5U1KkGZUdr+bIL4Y6BefeS549qoSHVSkUSs6A9INwdCND+/0+vN0NroPBXwl5Vcg5u78t7VQRsJjePxbw==} engines: {node: '>=22.12.0'} peerDependencies: - '@astrojs/markdown-satteri': ^0.3.1-alpha.0 - astro: ^7.0.0-alpha.0 + '@astrojs/markdown-satteri': ^0.3.1 + astro: ^7.0.0 peerDependenciesMeta: '@astrojs/markdown-satteri': optional: true @@ -180,8 +181,8 @@ packages: resolution: {integrity: sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==} engines: {node: '>=22.12.0'} - '@astrojs/react@6.0.0': - resolution: {integrity: sha512-jNf3kKE6KYXJbD5ZsXaLhnwDK3YvK9ttQ2ykAcNf5XdxOlUQEHLnomO3FO8abqghs0ilqhgGOyc2AlgGyQtz/g==} + '@astrojs/react@6.0.1': + resolution: {integrity: sha512-Afs1sEm72P2plDnrOGxmIteJ7bjx/VqxlcaQLNip5eHJ5tIvKUORQetC9UKcvgwKnj51t60HWl5mOANkOsWs4w==} engines: {node: '>=22.12.0'} peerDependencies: '@types/react': ^17.0.50 || ^18.0.21 || ^19.0.0 @@ -192,8 +193,8 @@ packages: '@astrojs/sitemap@3.7.3': resolution: {integrity: sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA==} - '@astrojs/starlight@0.41.1': - resolution: {integrity: sha512-avf2OmrVg6GdVU18juebjjIIuLa+uS3syHuJ/3yDaEFP/8it+YvcxRrYDSf7K6rC4v770UxIddba2hAqQyTeYA==} + '@astrojs/starlight@0.41.3': + resolution: {integrity: sha512-8xsG6UpK581TJmtOc7/pnxHKEbP16rV06VLWaVa7FOyE/VEwnaHOsLtL+miifCEfuiuv0Wn+j8u3JSluRQesMQ==} peerDependencies: '@astrojs/markdown-remark': ^7.2.0 astro: ^7.0.2 @@ -721,52 +722,52 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} - '@bruits/satteri-darwin-arm64@0.9.3': - resolution: {integrity: sha512-dRUZZrdwh1asfTOyM1nDNmzolhnHtlIFpqYrl1Tdd3YVcaebKmrfJgGL7NAoGPjbEwYmZxaugrxA0uzw83c0dw==} + '@bruits/satteri-darwin-arm64@0.9.4': + resolution: {integrity: sha512-W3MSUkr2mZRR8Stoe+lqNAyzQzRuFMU8WffV9IvFSxTok0LGWR0ZZQPLELU4QTRiUbhL2Y4VUP9vV7pj8rHjgg==} cpu: [arm64] os: [darwin] - '@bruits/satteri-darwin-x64@0.9.3': - resolution: {integrity: sha512-wgNCTRp2hPSpNMGFv5A4+6+VXgRJIlBZ7XKb3iwjV8YjRWNIjzE5zV2fUeYynyZYVRkuJ9aYFqQmWhc1e5H+UQ==} + '@bruits/satteri-darwin-x64@0.9.4': + resolution: {integrity: sha512-DXOuuaE1lsv7mpk2mOvGrzqoEWEvOIZEO/fXVa7zfM23Iob+CBjBkRAMwpHA4pmZ3j6Gj7WJzPKw0kQ7w741AQ==} cpu: [x64] os: [darwin] - '@bruits/satteri-linux-arm64-gnu@0.9.3': - resolution: {integrity: sha512-A/pWy8Jb/PhDYc2/JFuYh06gFJcsfBUBDl81YydGYBrL/Z4nItDfhNDNOibyeSN/lKKDRlycIHEIajjErk00sQ==} + '@bruits/satteri-linux-arm64-gnu@0.9.4': + resolution: {integrity: sha512-gJxU9rGGoqIznSEgEzpjxkry24jeHuMpoo1tCIAhHYh7WaD3j5F8zt3jmHxEaN1Uwa+K5+wFgIR2uIGOnMzEmw==} cpu: [arm64] os: [linux] libc: [glibc] - '@bruits/satteri-linux-arm64-musl@0.9.3': - resolution: {integrity: sha512-L6YxmyOSickzo4pE5WmZfNTJnjX0MtgKOsuwQfNZECTx9Ir5vl2B37EIwnxe2AybuPPHl+FqVQtthNDUdH4Vgg==} + '@bruits/satteri-linux-arm64-musl@0.9.4': + resolution: {integrity: sha512-Wjzu9hmmAbfmDkBfPI1VdZygJtYz9uYZQnkEyrXi6S2JFi+2pXQ1A5irj38bqm0IZmWcTbk0cVG4NZnPdtVNJA==} cpu: [arm64] os: [linux] libc: [musl] - '@bruits/satteri-linux-x64-gnu@0.9.3': - resolution: {integrity: sha512-RgH6GPihg9Lzs2yHUsMjqiLxfLyOdmBty8sg9pBY9B4CBnvdOzvg8vklqN+C4qrEEdA9TwpbDpHr1AshLKyRpw==} + '@bruits/satteri-linux-x64-gnu@0.9.4': + resolution: {integrity: sha512-MR1Q+wMx65FQlbSV7cRqWW87Knp0zkoaIV55Dt+xZl028wJABXEPEEmG3670SLq7lVZvcGIDwCgSg2kCYxvRwA==} cpu: [x64] os: [linux] libc: [glibc] - '@bruits/satteri-linux-x64-musl@0.9.3': - resolution: {integrity: sha512-BeWhVORjNTIomePznUKiMbHZTqC0j7sMXZFsISmbX+po5d33KLkqBqKh6K332CHJ8KUmCWx16FfPjwsoysttQg==} + '@bruits/satteri-linux-x64-musl@0.9.4': + resolution: {integrity: sha512-T4gxhXve3zyNAZesrXAd/rDZOGRkbfFIUFld4TGsw6BsjoIteCcDji6IMqeXyaWEVSykY2X8Eid2hr6aXGYAaw==} cpu: [x64] os: [linux] libc: [musl] - '@bruits/satteri-wasm32-wasi@0.9.3': - resolution: {integrity: sha512-dFNcOHKWV2cztCPnYTn7kZ9D7kNOt8N239z5ysFkNHLxJrfK7zaKIXQbfXYN32C+JoVFqAcTIOeWH2+VnsCOHg==} + '@bruits/satteri-wasm32-wasi@0.9.4': + resolution: {integrity: sha512-/CEG8LUlpaBEnhFnYVn0UnlHFLs51UhrkJBUPDUXLzkadzAcnR88iRA/nOl7Zwhjb4WhfBV4p3P5qeOJMtH0iA==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@bruits/satteri-win32-arm64-msvc@0.9.3': - resolution: {integrity: sha512-VnwjBHiAra/PNNEza8eSZdQiG4A3PtTJJwUDtOPAc6iTs0BWZwZX8+OPUZE7//yQCBhgvEMcI8vpwsAwCb6qGQ==} + '@bruits/satteri-win32-arm64-msvc@0.9.4': + resolution: {integrity: sha512-E1ZPQbgCtFKiU7pFYVndynvY7ne4coeVDUgnVThErSFlJ2ceQCBZrfRTD1lzrIDy63Bbqo+g/cZY9duw+JYjIw==} cpu: [arm64] os: [win32] - '@bruits/satteri-win32-x64-msvc@0.9.3': - resolution: {integrity: sha512-Dsoe4reWe69MyILmMwU6iISIceTW7YIFqbyym7haf9DhUvqkYfMAyp7GMM21JzV0SpG9A2BwzFVP7iq9mmxrpA==} + '@bruits/satteri-win32-x64-msvc@0.9.4': + resolution: {integrity: sha512-5I7SiarsNdAUuhJb50CXJPTwr/ECVrBoU+fymoLjChK5fW//+srhY4lstcNTzgFRtQSYfVtm4OQZz16CVMeTeA==} cpu: [x64] os: [win32] @@ -774,12 +775,12 @@ packages: resolution: {integrity: sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==} engines: {node: '>=18'} - '@clack/core@1.4.2': - resolution: {integrity: sha512-0Ty/1Gfm+Kb07sXcuESjyKfwEhSy4Ns1AgeEisHb/bDY5fWme0tTeTkU14T1Gmcs17YIjB/teiDe4uaCghbYqQ==} + '@clack/core@1.4.3': + resolution: {integrity: sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==} engines: {node: '>= 20.12.0'} - '@clack/prompts@1.6.0': - resolution: {integrity: sha512-EYlRokl8szrP9Z25qT5aepMdBjzBvHF9ZEhzIiUBc9guz/T31EqRgvD0QSgZcpE93xiwrr+OkB4nz0BZyF6fSA==} + '@clack/prompts@1.7.0': + resolution: {integrity: sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==} engines: {node: '>= 20.12.0'} '@ctrl/tinycolor@4.2.0': @@ -813,6 +814,9 @@ packages: '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} @@ -988,309 +992,160 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - - '@img/sharp-darwin-arm64@0.35.2': - resolution: {integrity: sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==} + '@img/sharp-darwin-arm64@0.35.3': + resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - - '@img/sharp-darwin-x64@0.35.2': - resolution: {integrity: sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==} + '@img/sharp-darwin-x64@0.35.3': + resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-freebsd-wasm32@0.35.2': - resolution: {integrity: sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==} + '@img/sharp-freebsd-wasm32@0.35.3': + resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} engines: {node: '>=20.9.0'} os: [freebsd] - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + '@img/sharp-libvips-darwin-arm64@1.3.2': + resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.3.1': - resolution: {integrity: sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==} - cpu: [arm64] - os: [darwin] - - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + '@img/sharp-libvips-darwin-x64@1.3.2': + resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.1': - resolution: {integrity: sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-arm64@1.3.1': - resolution: {integrity: sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==} + '@img/sharp-libvips-linux-arm64@1.3.2': + resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-arm@1.3.1': - resolution: {integrity: sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==} + '@img/sharp-libvips-linux-arm@1.3.2': + resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + '@img/sharp-libvips-linux-ppc64@1.3.2': + resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.3.1': - resolution: {integrity: sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-riscv64@1.3.1': - resolution: {integrity: sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==} + '@img/sharp-libvips-linux-riscv64@1.3.2': + resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + '@img/sharp-libvips-linux-s390x@1.3.2': + resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.3.1': - resolution: {integrity: sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-x64@1.3.1': - resolution: {integrity: sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==} + '@img/sharp-libvips-linux-x64@1.3.2': + resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-arm64@1.3.1': - resolution: {integrity: sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@img/sharp-libvips-linuxmusl-x64@1.3.1': - resolution: {integrity: sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==} + '@img/sharp-libvips-linuxmusl-x64@1.3.2': + resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-arm64@0.35.2': - resolution: {integrity: sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==} + '@img/sharp-linux-arm64@0.35.3': + resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-arm@0.35.2': - resolution: {integrity: sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==} + '@img/sharp-linux-arm@0.35.3': + resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-ppc64@0.35.2': - resolution: {integrity: sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==} + '@img/sharp-linux-ppc64@0.35.3': + resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-riscv64@0.35.2': - resolution: {integrity: sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==} + '@img/sharp-linux-riscv64@0.35.3': + resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-s390x@0.35.2': - resolution: {integrity: sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==} + '@img/sharp-linux-s390x@0.35.3': + resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-x64@0.35.2': - resolution: {integrity: sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==} + '@img/sharp-linux-x64@0.35.3': + resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@img/sharp-linuxmusl-arm64@0.35.2': - resolution: {integrity: sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==} + '@img/sharp-linuxmusl-arm64@0.35.3': + resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@img/sharp-linuxmusl-x64@0.35.2': - resolution: {integrity: sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==} + '@img/sharp-linuxmusl-x64@0.35.3': + resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - - '@img/sharp-wasm32@0.35.2': - resolution: {integrity: sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==} + '@img/sharp-wasm32@0.35.3': + resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} engines: {node: '>=20.9.0'} - '@img/sharp-webcontainers-wasm32@0.35.2': - resolution: {integrity: sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==} + '@img/sharp-webcontainers-wasm32@0.35.3': + resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [win32] - - '@img/sharp-win32-arm64@0.35.2': - resolution: {integrity: sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==} + '@img/sharp-win32-arm64@0.35.3': + resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - - '@img/sharp-win32-ia32@0.35.2': - resolution: {integrity: sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==} + '@img/sharp-win32-ia32@0.35.3': + resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - - '@img/sharp-win32-x64@0.35.2': - resolution: {integrity: sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==} + '@img/sharp-win32-x64@0.35.3': + resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -1620,30 +1475,58 @@ packages: resolution: {integrity: sha512-EooU3i9F6IAE8kEu+AnGf9DFZWkQBZ+hJn3tLVbsH+61mtQiva5biai66fAA6nvFPXkLgvrh7BrR7YcJU83xQQ==} engines: {node: '>=20'} + '@shikijs/core@4.3.1': + resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==} + engines: {node: '>=20'} + '@shikijs/engine-javascript@4.3.0': resolution: {integrity: sha512-hTv/KiFf2tpiqlACPiztGGurEARWIutB8YUhcrA1pUC7VzzwKO+g5crUocrLztrZ5ro5Z4hbXg7bYclETn3gSQ==} engines: {node: '>=20'} + '@shikijs/engine-javascript@4.3.1': + resolution: {integrity: sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==} + engines: {node: '>=20'} + '@shikijs/engine-oniguruma@4.3.0': resolution: {integrity: sha512-1vMdN3gHfnKfLYwecUI2ITJI4RhHt96xEaJumVn7Heb0IlJ8WQMIH0Voak+2j22BpSNKdnOfB/pCTPnPm2gq7A==} engines: {node: '>=20'} + '@shikijs/engine-oniguruma@4.3.1': + resolution: {integrity: sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==} + engines: {node: '>=20'} + '@shikijs/langs@4.3.0': resolution: {integrity: sha512-rnlqFbBRSys9bT4gl/5rw9RnS0W/I84ZldXPkO7cvlEMoV85TyF/aU01N7/NbSR776RNLjrJKjfFUXJR6wN1Cg==} engines: {node: '>=20'} + '@shikijs/langs@4.3.1': + resolution: {integrity: sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==} + engines: {node: '>=20'} + '@shikijs/primitive@4.3.0': resolution: {integrity: sha512-CPkz64PTa5diRW1ggzMZH9VM/du4RNChYgVtgqrFcgruvIybmCvySv8GkiHSczUHXYuuR8TdKEwFx+UnZMpgdg==} engines: {node: '>=20'} + '@shikijs/primitive@4.3.1': + resolution: {integrity: sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==} + engines: {node: '>=20'} + '@shikijs/themes@4.3.0': resolution: {integrity: sha512-Avgt05YiT+Y3prjIc9lmQxhJzHBcCfR6cjiFW4OyaMBbt2A6trX5rfjUzx+Vj/mE9qpArYjatnqo9XPjQNW/AQ==} engines: {node: '>=20'} + '@shikijs/themes@4.3.1': + resolution: {integrity: sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==} + engines: {node: '>=20'} + '@shikijs/types@4.3.0': resolution: {integrity: sha512-oc8b9U2SYvofKZk8e/737nIX0qwf6eV2vHFATeObAu7r+mUVpLs8Re0BmVkIjAWAYgkmG/CzLNo7rzuBzRu/wQ==} engines: {node: '>=20'} + '@shikijs/types@4.3.1': + resolution: {integrity: sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==} + engines: {node: '>=20'} + '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -1698,6 +1581,9 @@ packages: '@types/node@24.13.2': resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==} + '@types/node@26.1.0': + resolution: {integrity: sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -1784,6 +1670,11 @@ packages: ajv: optional: true + ajv-i18n@4.2.0: + resolution: {integrity: sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg==} + peerDependencies: + ajv: ^8.0.0-beta.0 + ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} @@ -1833,12 +1724,12 @@ packages: peerDependencies: astro: ^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0 - astro@7.0.3: - resolution: {integrity: sha512-CK+G+Tl2DMV1EXCwVG45vyurxf2IfRTklMxDhRKn+tst9Yl8rWXpudL62Fa6zin5Bt968FBvuyASj1aJShROZg==} + astro@7.0.6: + resolution: {integrity: sha512-Myw0sFia+zs/Y0yqfZEsUYXfDPh3ELcLf1f0Q/qQzVXBh/af1qO62WNT+P89DCcfGVV51nMoQhEfkBYqJmoUOQ==} engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true peerDependencies: - '@astrojs/markdown-remark': 7.2.0 + '@astrojs/markdown-remark': 7.2.1 peerDependenciesMeta: '@astrojs/markdown-remark': optional: true @@ -1891,8 +1782,8 @@ packages: bcp-47-match@2.0.3: resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==} - bcp-47@2.1.0: - resolution: {integrity: sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==} + bcp-47@2.1.1: + resolution: {integrity: sha512-KLw+H/gd2p4zly1X7Yh/qziuyae5/w/QFnvTng9eZL5fvszL7Whl3MBoWF8yxL7ksUjBfOD+OxkytiqbBpG+Fw==} boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -2169,8 +2060,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@2.2.0: - resolution: {integrity: sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ==} + es-module-lexer@2.3.0: + resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==} es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} @@ -2457,8 +2348,8 @@ packages: http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} - i18next@26.3.3: - resolution: {integrity: sha512-aYVegyBdXSO93CMMihvr47jI7GHSOcIahMpJX+qzUXDzW4xDJf2uenIA+45vDU+YhiVdcfsql70AC9RVdMNrHg==} + i18next@26.3.4: + resolution: {integrity: sha512-pa7m0d7pBDqGHZxljT+WPFeyFgQ7P7SciPPo1tTqYuO0z4sqADYhwnBESmmGp/wEof1inwdls/k8ZgTg8rxFHA==} peerDependencies: typescript: ^5 || ^6 peerDependenciesMeta: @@ -2468,8 +2359,8 @@ packages: idb@7.1.1: resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} - immutable@5.1.8: - resolution: {integrity: sha512-TM5YqrGeTsVIPPpILzeqZ8D2Zc2TvNgSDi88zPF2a4cyqQdWV/wVWBDRDbNzzrLeRWScrFcOX9lW2iX6GOtUDw==} + immutable@5.1.9: + resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==} inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} @@ -3068,16 +2959,16 @@ packages: resolution: {integrity: sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==} engines: {node: '>=20'} - p-queue@9.3.0: - resolution: {integrity: sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==} + p-queue@9.3.1: + resolution: {integrity: sha512-POWdiIPmsUPGwb4FeQ4OBg46aqmcInSWe45CKDsGHiOBiVQM9chqfQTuqhuTzcg2Vz9faTI65at0KkVyVEiCHw==} engines: {node: '>=20'} p-timeout@7.0.1: resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} engines: {node: '>=20'} - package-manager-detector@1.6.0: - resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + package-manager-detector@1.7.0: + resolution: {integrity: sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ==} pagefind@1.5.2: resolution: {integrity: sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==} @@ -3116,6 +3007,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -3134,8 +3029,8 @@ packages: resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} - prettier@3.9.3: - resolution: {integrity: sha512-HWmu+K+zvHNpaMfSnYeqdqrDbR16cuIXaPx8WoHaviQkDJh1/0BNtOZmHVQI5jc3wXv0H1yXc9wjvFdXh+n3hQ==} + prettier@3.9.4: + resolution: {integrity: sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==} engines: {node: '>=14'} hasBin: true @@ -3345,8 +3240,8 @@ packages: engines: {node: '>=20.19.0'} hasBin: true - satteri@0.9.3: - resolution: {integrity: sha512-2XfBh89LCnBMFkNOeVKkBLelAZcIA17VLHsgJum1tJ2fXiPZDN/TDXv4ku46rFOQXYd41LJ0kiZh5gPqExcCsg==} + satteri@0.9.4: + resolution: {integrity: sha512-BKob126Tay84diOZsnVNH/Q/c+3njPJTCad3w5zLKa6j8bVjxskPNHDtxrMwYK4bN/RlqUSdMnPwKY4k65EMOQ==} sax@1.6.0: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} @@ -3379,18 +3274,23 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - - sharp@0.35.2: - resolution: {integrity: sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==} + sharp@0.35.3: + resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true shiki@4.3.0: resolution: {integrity: sha512-NKKjWzR6LIGL3sXBrWDw9sDS9cxx42/DkysaNqJEeOWE8Kix5gpak0bc00OfDVEO4oyXSyz8+aRaqKoBD1yo7A==} engines: {node: '>=20'} + shiki@4.3.1: + resolution: {integrity: sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==} + engines: {node: '>=20'} + side-channel-list@1.0.1: resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} @@ -3594,6 +3494,9 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} @@ -3754,8 +3657,8 @@ packages: '@vite-pwa/assets-generator': optional: true - vite@8.1.0: - resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==} + vite@8.1.3: + resolution: {integrity: sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3805,32 +3708,32 @@ packages: vite: optional: true - volar-service-css@0.0.70: - resolution: {integrity: sha512-K1qyOvBpE3rzdAv3e4/6Rv5yizrYPy5R/ne3IWCAzLBuMO4qBMV3kSqWzj6KUVe6S0AnN6wxF7cRkiaKfYMYJw==} + volar-service-css@0.0.71: + resolution: {integrity: sha512-wRRFt9BpjMKCazcgOh67MSjUjiWUCAh99DyYSDIOTuxaRjEtDC7PpB0k1Y1wbJIW/pVtMUSVbpPo3UGSm0Byxw==} peerDependencies: '@volar/language-service': ~2.4.0 peerDependenciesMeta: '@volar/language-service': optional: true - volar-service-emmet@0.0.70: - resolution: {integrity: sha512-xi5bC4m/VyE3zy/n2CXspKeDZs3qA41tHLTw275/7dNWM/RqE2z3BnDICQybHIVp/6G1iOQj5c1qXMgQC08TNg==} + volar-service-emmet@0.0.71: + resolution: {integrity: sha512-zqjzt6bN95e3CUstBm0PBFAJnrfz0ZAARka87fart46/gNCLLuP3Vujy8V/J8HEziTFLnfkgIASLFYPUhonJcA==} peerDependencies: '@volar/language-service': ~2.4.0 peerDependenciesMeta: '@volar/language-service': optional: true - volar-service-html@0.0.70: - resolution: {integrity: sha512-eR6vCgMdmYAo4n+gcT7DSyBQbwB8S3HZZvSagTf0sxNaD4WppMCFfpqWnkrlGStPKMZvMiejRRVmqsX9dYcTvQ==} + volar-service-html@0.0.71: + resolution: {integrity: sha512-e8tHPhgQ7ooLfudAEIku+kgd9pWkq3SSz8RbnQDI1+Eb8wbenkLGHqoirLqz5ORLV6wIMr2Iv08RWBG5eOcgpw==} peerDependencies: '@volar/language-service': ~2.4.0 peerDependenciesMeta: '@volar/language-service': optional: true - volar-service-prettier@0.0.70: - resolution: {integrity: sha512-Z6BCFSpGVCd8BPAsZ785Kce1BGlWd5ODqmqZGVuB14MJvrR4+CYz6cDy4F+igmE1gMifqfvMhdgT8Aud4M5ngg==} + volar-service-prettier@0.0.71: + resolution: {integrity: sha512-Rz7JVH3qD108UCdmIEiZvOBNljMt2nLFdbN8AXcDfn7xD9F5I2aCIsDVqBbXw21PsnxG0b7MfwtNF+zPS/NKUg==} peerDependencies: '@volar/language-service': ~2.4.0 prettier: ^2.2 || ^3.0 @@ -3840,24 +3743,24 @@ packages: prettier: optional: true - volar-service-typescript-twoslash-queries@0.0.70: - resolution: {integrity: sha512-IdD13Z9N2Bu8EM6CM0fDV1E69olEYGHDU25X51YXmq8Y0CmJ2LNj6gOiBJgpS5JGUqFzECVhMNBW7R0sPdRTMQ==} + volar-service-typescript-twoslash-queries@0.0.71: + resolution: {integrity: sha512-9K2k72s4n7rV9s4bX0MyjbX9iBribvKZbBJKuEmTCZfeWJXs6Yh7bGpY4eoc7UufAjvpheBqwyZCOIPBvxCv0A==} peerDependencies: '@volar/language-service': ~2.4.0 peerDependenciesMeta: '@volar/language-service': optional: true - volar-service-typescript@0.0.70: - resolution: {integrity: sha512-l46Bx4cokkUedTd74ojO5H/zqHZJ8SUuyZ0IB8JN4jfRqUM3bQFBHoOwlZCyZmOeO0A3RQNkMnFclxO4c++gsg==} + volar-service-typescript@0.0.71: + resolution: {integrity: sha512-yTtM/BVT6hoyEYnDtaCyAtNhdNeS/mhTTABlBOdw3NNiRBUin3IznFJpgfjer4c6RYopiPjjQjc9VFhxVl1mLw==} peerDependencies: '@volar/language-service': ~2.4.0 peerDependenciesMeta: '@volar/language-service': optional: true - volar-service-yaml@0.0.70: - resolution: {integrity: sha512-0c8bXDBeoATF9F6iPIlOuYTuZAC4c+yi0siQo920u7eiBJk8oQmUmg9cDUbR4+Gl++bvGP4plj3fErbJuPqdcQ==} + volar-service-yaml@0.0.71: + resolution: {integrity: sha512-qYGWGuVpUTnZGu5P/CR4KLK4aIR8RrcVnmfZ2eRcj9q/I8VZCoC5yy9FtEvfNvnDp4MU17yhdJcvpQPIqhJS2Q==} peerDependencies: '@volar/language-service': ~2.4.0 peerDependenciesMeta: @@ -3878,15 +3781,15 @@ packages: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'} - vscode-jsonrpc@9.0.0: - resolution: {integrity: sha512-+VvMmQPJhtvJ+8O+zu2JKIRiLxXF8NW7krWgyMGeOHrp4Cn23T5hc0v2LknNeopDOB70wghHAds7mKtcZ0I4Sg==} + vscode-jsonrpc@9.0.1: + resolution: {integrity: sha512-rfuA6T75H6m5EkbhtEPzre9pT0HPcDI2MMy4+nPFIBks5J8JBAUHD4tRYSgaBOijIEC7SRkC1kKyXTLqbmh9jw==} engines: {node: '>=14.0.0'} vscode-languageserver-protocol@3.17.5: resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} - vscode-languageserver-protocol@3.18.1: - resolution: {integrity: sha512-RTiiVHdpxpYcJVI5sq6S5TLjQ4WDR/rBrIWru+kPXe6sGQ9PFQ3GamrTKLvPqbR4ylr1SoodhmcqbFII0WXVuw==} + vscode-languageserver-protocol@3.18.2: + resolution: {integrity: sha512-XRyDbT0Pp3sSNti3JmxVEUMySWCSi1hhM+/KUlCy1hV1zmrqpM1OwO12EAki8blhmLuIMpaJrYbo0OzGVfK2Qg==} vscode-languageserver-textdocument@1.0.12: resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} @@ -4008,13 +3911,13 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yaml-language-server@1.20.0: - resolution: {integrity: sha512-qhjK/bzSRZ6HtTvgeFvjNPJGWdZ0+x5NREV/9XZWFjIGezew2b4r5JPy66IfOhd5OA7KeFwk1JfmEbnTvev0cA==} + yaml-language-server@1.23.0: + resolution: {integrity: sha512-3qVyCOexLCWw06PQa5kRPwvMWMZ/eZeCRWUvgD6a0OkqL/4iCnxy2WumbWifa937Uo5xhyWJ0uxlU39ljhNh7A==} hasBin: true - yaml@2.7.1: - resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==} - engines: {node: '>= 14'} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} hasBin: true yaml@2.9.0: @@ -4052,9 +3955,9 @@ snapshots: jsonpointer: 5.0.1 leven: 3.1.0 - '@astrojs/check@0.9.9(prettier@3.9.3)(typescript@6.0.3)': + '@astrojs/check@0.9.9(prettier@3.9.4)(typescript@6.0.3)': dependencies: - '@astrojs/language-server': 2.16.10(prettier@3.9.3)(typescript@6.0.3) + '@astrojs/language-server': 2.16.11(prettier@3.9.4)(typescript@6.0.3) chokidar: 4.0.3 kleur: 4.1.5 typescript: 6.0.3 @@ -4063,63 +3966,63 @@ snapshots: - prettier - prettier-plugin-astro - '@astrojs/compiler-binding-darwin-arm64@0.2.3': + '@astrojs/compiler-binding-darwin-arm64@0.3.0': optional: true - '@astrojs/compiler-binding-darwin-x64@0.2.3': + '@astrojs/compiler-binding-darwin-x64@0.3.0': optional: true - '@astrojs/compiler-binding-linux-arm64-gnu@0.2.3': + '@astrojs/compiler-binding-linux-arm64-gnu@0.3.0': optional: true - '@astrojs/compiler-binding-linux-arm64-musl@0.2.3': + '@astrojs/compiler-binding-linux-arm64-musl@0.3.0': optional: true - '@astrojs/compiler-binding-linux-x64-gnu@0.2.3': + '@astrojs/compiler-binding-linux-x64-gnu@0.3.0': optional: true - '@astrojs/compiler-binding-linux-x64-musl@0.2.3': + '@astrojs/compiler-binding-linux-x64-musl@0.3.0': optional: true - '@astrojs/compiler-binding-wasm32-wasi@0.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + '@astrojs/compiler-binding-wasm32-wasi@0.3.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)': dependencies: - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' optional: true - '@astrojs/compiler-binding-win32-arm64-msvc@0.2.3': + '@astrojs/compiler-binding-win32-arm64-msvc@0.3.0': optional: true - '@astrojs/compiler-binding-win32-x64-msvc@0.2.3': + '@astrojs/compiler-binding-win32-x64-msvc@0.3.0': optional: true - '@astrojs/compiler-binding@0.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + '@astrojs/compiler-binding@0.3.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)': optionalDependencies: - '@astrojs/compiler-binding-darwin-arm64': 0.2.3 - '@astrojs/compiler-binding-darwin-x64': 0.2.3 - '@astrojs/compiler-binding-linux-arm64-gnu': 0.2.3 - '@astrojs/compiler-binding-linux-arm64-musl': 0.2.3 - '@astrojs/compiler-binding-linux-x64-gnu': 0.2.3 - '@astrojs/compiler-binding-linux-x64-musl': 0.2.3 - '@astrojs/compiler-binding-wasm32-wasi': 0.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) - '@astrojs/compiler-binding-win32-arm64-msvc': 0.2.3 - '@astrojs/compiler-binding-win32-x64-msvc': 0.2.3 + '@astrojs/compiler-binding-darwin-arm64': 0.3.0 + '@astrojs/compiler-binding-darwin-x64': 0.3.0 + '@astrojs/compiler-binding-linux-arm64-gnu': 0.3.0 + '@astrojs/compiler-binding-linux-arm64-musl': 0.3.0 + '@astrojs/compiler-binding-linux-x64-gnu': 0.3.0 + '@astrojs/compiler-binding-linux-x64-musl': 0.3.0 + '@astrojs/compiler-binding-wasm32-wasi': 0.3.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2) + '@astrojs/compiler-binding-win32-arm64-msvc': 0.3.0 + '@astrojs/compiler-binding-win32-x64-msvc': 0.3.0 transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' - '@astrojs/compiler-rs@0.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + '@astrojs/compiler-rs@0.3.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)': dependencies: - '@astrojs/compiler-binding': 0.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@astrojs/compiler-binding': 0.3.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' '@astrojs/compiler@2.13.1': {} - '@astrojs/internal-helpers@0.10.0': + '@astrojs/internal-helpers@0.10.1': dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 @@ -4130,7 +4033,7 @@ snapshots: smol-toml: 1.7.0 unified: 11.0.5 - '@astrojs/language-server@2.16.10(prettier@3.9.3)(typescript@6.0.3)': + '@astrojs/language-server@2.16.11(prettier@3.9.4)(typescript@6.0.3)': dependencies: '@astrojs/compiler': 2.13.1 '@astrojs/yaml2ts': 0.2.4 @@ -4141,23 +4044,23 @@ snapshots: '@volar/language-service': 2.4.28 muggle-string: 0.4.1 tinyglobby: 0.2.17 - volar-service-css: 0.0.70(@volar/language-service@2.4.28) - volar-service-emmet: 0.0.70(@volar/language-service@2.4.28) - volar-service-html: 0.0.70(@volar/language-service@2.4.28) - volar-service-prettier: 0.0.70(@volar/language-service@2.4.28)(prettier@3.9.3) - volar-service-typescript: 0.0.70(@volar/language-service@2.4.28) - volar-service-typescript-twoslash-queries: 0.0.70(@volar/language-service@2.4.28) - volar-service-yaml: 0.0.70(@volar/language-service@2.4.28) + volar-service-css: 0.0.71(@volar/language-service@2.4.28) + volar-service-emmet: 0.0.71(@volar/language-service@2.4.28) + volar-service-html: 0.0.71(@volar/language-service@2.4.28) + volar-service-prettier: 0.0.71(@volar/language-service@2.4.28)(prettier@3.9.4) + volar-service-typescript: 0.0.71(@volar/language-service@2.4.28) + volar-service-typescript-twoslash-queries: 0.0.71(@volar/language-service@2.4.28) + volar-service-yaml: 0.0.71(@volar/language-service@2.4.28) vscode-html-languageservice: 5.6.2 vscode-uri: 3.1.0 optionalDependencies: - prettier: 3.9.3 + prettier: 3.9.4 transitivePeerDependencies: - typescript - '@astrojs/markdown-remark@7.2.0': + '@astrojs/markdown-remark@7.2.1': dependencies: - '@astrojs/internal-helpers': 0.10.0 + '@astrojs/internal-helpers': 0.10.1 '@astrojs/prism': 4.0.2 github-slugger: 2.0.0 hast-util-from-html: 2.0.3 @@ -4177,21 +4080,21 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/markdown-satteri@0.3.2': + '@astrojs/markdown-satteri@0.3.3': dependencies: - '@astrojs/internal-helpers': 0.10.0 + '@astrojs/internal-helpers': 0.10.1 '@astrojs/prism': 4.0.2 github-slugger: 2.0.0 - satteri: 0.9.3 + satteri: 0.9.4 - '@astrojs/mdx@7.0.0(@astrojs/markdown-satteri@0.3.2)(astro@7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': + '@astrojs/mdx@7.0.2(@astrojs/markdown-satteri@0.3.3)(astro@7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: - '@astrojs/internal-helpers': 0.10.0 - '@astrojs/markdown-remark': 7.2.0 + '@astrojs/internal-helpers': 0.10.1 + '@astrojs/markdown-remark': 7.2.1 '@mdx-js/mdx': 3.1.1 acorn: 8.17.0 - astro: 7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - es-module-lexer: 2.2.0 + astro: 7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + es-module-lexer: 2.3.0 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 piccolore: 0.1.3 @@ -4202,7 +4105,7 @@ snapshots: unist-util-visit: 5.1.0 vfile: 6.0.3 optionalDependencies: - '@astrojs/markdown-satteri': 0.3.2 + '@astrojs/markdown-satteri': 0.3.3 transitivePeerDependencies: - supports-color @@ -4210,17 +4113,17 @@ snapshots: dependencies: prismjs: 1.30.0 - '@astrojs/react@6.0.0(@types/node@24.13.2)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)': + '@astrojs/react@6.0.1(@types/node@26.1.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)': dependencies: - '@astrojs/internal-helpers': 0.10.0 + '@astrojs/internal-helpers': 0.10.1 '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@vitejs/plugin-react': 5.2.0(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + '@vitejs/plugin-react': 5.2.0(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) devalue: 5.8.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) ultrahtml: 1.6.0 - vite: 8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - '@vitejs/devtools' @@ -4242,23 +4145,23 @@ snapshots: stream-replace-string: 2.0.0 zod: 4.4.3 - '@astrojs/starlight@0.41.1(@astrojs/markdown-remark@7.2.0)(astro@7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3)': + '@astrojs/starlight@0.41.3(@astrojs/markdown-remark@7.2.1)(astro@7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3)': dependencies: - '@astrojs/markdown-satteri': 0.3.2 - '@astrojs/mdx': 7.0.0(@astrojs/markdown-satteri@0.3.2)(astro@7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + '@astrojs/markdown-satteri': 0.3.3 + '@astrojs/mdx': 7.0.2(@astrojs/markdown-satteri@0.3.3)(astro@7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) '@astrojs/sitemap': 3.7.3 '@pagefind/default-ui': 1.5.2 '@types/hast': 3.0.4 '@types/js-yaml': 4.0.9 '@types/mdast': 4.0.4 - astro: 7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - astro-expressive-code: 0.44.0(astro@7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) - bcp-47: 2.1.0 + astro: 7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + astro-expressive-code: 0.44.0(astro@7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + bcp-47: 2.1.1 hast-util-from-html: 2.0.3 hast-util-select: 6.0.4 hast-util-to-string: 3.0.1 hastscript: 9.0.1 - i18next: 26.3.3(typescript@6.0.3) + i18next: 26.3.4(typescript@6.0.3) js-yaml: 4.3.0 klona: 2.0.6 magic-string: 0.30.21 @@ -4269,13 +4172,13 @@ snapshots: rehype: 13.0.2 rehype-format: 5.0.1 remark-directive: 4.0.0 - satteri: 0.9.3 + satteri: 0.9.4 ultrahtml: 1.6.0 unified: 11.0.5 unist-util-visit: 5.1.0 vfile: 6.0.3 optionalDependencies: - '@astrojs/markdown-remark': 7.2.0 + '@astrojs/markdown-remark': 7.2.1 transitivePeerDependencies: - supports-color - typescript @@ -4965,49 +4868,49 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@bruits/satteri-darwin-arm64@0.9.3': + '@bruits/satteri-darwin-arm64@0.9.4': optional: true - '@bruits/satteri-darwin-x64@0.9.3': + '@bruits/satteri-darwin-x64@0.9.4': optional: true - '@bruits/satteri-linux-arm64-gnu@0.9.3': + '@bruits/satteri-linux-arm64-gnu@0.9.4': optional: true - '@bruits/satteri-linux-arm64-musl@0.9.3': + '@bruits/satteri-linux-arm64-musl@0.9.4': optional: true - '@bruits/satteri-linux-x64-gnu@0.9.3': + '@bruits/satteri-linux-x64-gnu@0.9.4': optional: true - '@bruits/satteri-linux-x64-musl@0.9.3': + '@bruits/satteri-linux-x64-musl@0.9.4': optional: true - '@bruits/satteri-wasm32-wasi@0.9.3': + '@bruits/satteri-wasm32-wasi@0.9.4': dependencies: '@emnapi/core': 1.11.1 '@emnapi/runtime': 1.11.1 '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@bruits/satteri-win32-arm64-msvc@0.9.3': + '@bruits/satteri-win32-arm64-msvc@0.9.4': optional: true - '@bruits/satteri-win32-x64-msvc@0.9.3': + '@bruits/satteri-win32-x64-msvc@0.9.4': optional: true '@capsizecss/unpack@4.0.1': dependencies: fontkitten: 1.0.3 - '@clack/core@1.4.2': + '@clack/core@1.4.3': dependencies: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 - '@clack/prompts@1.6.0': + '@clack/prompts@1.7.0': dependencies: - '@clack/core': 1.4.2 + '@clack/core': 1.4.3 fast-string-width: 3.0.2 fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 @@ -5048,6 +4951,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.2.2': dependencies: tslib: 2.8.1 @@ -5150,7 +5058,7 @@ snapshots: '@expressive-code/plugin-shiki@0.44.0': dependencies: '@expressive-code/core': 0.44.0 - shiki: 4.3.0 + shiki: 4.3.1 '@expressive-code/plugin-text-markers@0.44.0': dependencies: @@ -5158,202 +5066,108 @@ snapshots: '@img/colour@1.1.0': {} - '@img/sharp-darwin-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 - optional: true - - '@img/sharp-darwin-arm64@0.35.2': + '@img/sharp-darwin-arm64@0.35.3': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.3.1 + '@img/sharp-libvips-darwin-arm64': 1.3.2 optional: true - '@img/sharp-darwin-x64@0.34.5': + '@img/sharp-darwin-x64@0.35.3': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.3.2 optional: true - '@img/sharp-darwin-x64@0.35.2': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.3.1 - optional: true - - '@img/sharp-freebsd-wasm32@0.35.2': + '@img/sharp-freebsd-wasm32@0.35.3': dependencies: - '@img/sharp-wasm32': 0.35.2 - optional: true - - '@img/sharp-libvips-darwin-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-darwin-arm64@1.3.1': - optional: true - - '@img/sharp-libvips-darwin-x64@1.2.4': - optional: true - - '@img/sharp-libvips-darwin-x64@1.3.1': - optional: true - - '@img/sharp-libvips-linux-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm64@1.3.1': - optional: true - - '@img/sharp-libvips-linux-arm@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm@1.3.1': - optional: true - - '@img/sharp-libvips-linux-ppc64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-ppc64@1.3.1': - optional: true - - '@img/sharp-libvips-linux-riscv64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-riscv64@1.3.1': - optional: true - - '@img/sharp-libvips-linux-s390x@1.2.4': - optional: true - - '@img/sharp-libvips-linux-s390x@1.3.1': - optional: true - - '@img/sharp-libvips-linux-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-x64@1.3.1': - optional: true - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-arm64@1.3.1': + '@img/sharp-wasm32': 0.35.3 optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.4': + '@img/sharp-libvips-darwin-arm64@1.3.2': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.3.1': + '@img/sharp-libvips-darwin-x64@1.3.2': optional: true - '@img/sharp-linux-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-arm64@1.3.2': optional: true - '@img/sharp-linux-arm64@0.35.2': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.3.1 + '@img/sharp-libvips-linux-arm@1.3.2': optional: true - '@img/sharp-linux-arm@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-ppc64@1.3.2': optional: true - '@img/sharp-linux-arm@0.35.2': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.3.1 + '@img/sharp-libvips-linux-riscv64@1.3.2': optional: true - '@img/sharp-linux-ppc64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-s390x@1.3.2': optional: true - '@img/sharp-linux-ppc64@0.35.2': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.3.1 + '@img/sharp-libvips-linux-x64@1.3.2': optional: true - '@img/sharp-linux-riscv64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': optional: true - '@img/sharp-linux-riscv64@0.35.2': - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.3.1 + '@img/sharp-libvips-linuxmusl-x64@1.3.2': optional: true - '@img/sharp-linux-s390x@0.34.5': + '@img/sharp-linux-arm64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.3.2 optional: true - '@img/sharp-linux-s390x@0.35.2': + '@img/sharp-linux-arm@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.3.1 + '@img/sharp-libvips-linux-arm': 1.3.2 optional: true - '@img/sharp-linux-x64@0.34.5': + '@img/sharp-linux-ppc64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.3.2 optional: true - '@img/sharp-linux-x64@0.35.2': + '@img/sharp-linux-riscv64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.3.1 + '@img/sharp-libvips-linux-riscv64': 1.3.2 optional: true - '@img/sharp-linuxmusl-arm64@0.34.5': + '@img/sharp-linux-s390x@0.35.3': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.3.2 optional: true - '@img/sharp-linuxmusl-arm64@0.35.2': + '@img/sharp-linux-x64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 + '@img/sharp-libvips-linux-x64': 1.3.2 optional: true - '@img/sharp-linuxmusl-x64@0.34.5': + '@img/sharp-linuxmusl-arm64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 optional: true - '@img/sharp-linuxmusl-x64@0.35.2': + '@img/sharp-linuxmusl-x64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.3.1 + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 optional: true - '@img/sharp-wasm32@0.34.5': + '@img/sharp-wasm32@0.35.3': dependencies: - '@emnapi/runtime': 1.11.1 + '@emnapi/runtime': 1.11.2 optional: true - '@img/sharp-wasm32@0.35.2': + '@img/sharp-webcontainers-wasm32@0.35.3': dependencies: - '@emnapi/runtime': 1.11.1 - optional: true - - '@img/sharp-webcontainers-wasm32@0.35.2': - dependencies: - '@img/sharp-wasm32': 0.35.2 - optional: true - - '@img/sharp-win32-arm64@0.34.5': + '@img/sharp-wasm32': 0.35.3 optional: true - '@img/sharp-win32-arm64@0.35.2': + '@img/sharp-win32-arm64@0.35.3': optional: true - '@img/sharp-win32-ia32@0.34.5': + '@img/sharp-win32-ia32@0.35.3': optional: true - '@img/sharp-win32-ia32@0.35.2': - optional: true - - '@img/sharp-win32-x64@0.34.5': - optional: true - - '@img/sharp-win32-x64@0.35.2': + '@img/sharp-win32-x64@0.35.3': optional: true '@jridgewell/gen-mapping@0.3.13': @@ -5419,6 +5233,13 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.2 + '@tybys/wasm-util': 0.10.3 + optional: true + '@oslojs/encoding@1.1.0': {} '@oxc-project/types@0.137.0': {} @@ -5490,7 +5311,7 @@ snapshots: detect-libc: 2.1.2 is-glob: 4.0.3 node-addon-api: 7.1.1 - picomatch: 4.0.4 + picomatch: 4.0.5 optionalDependencies: '@parcel/watcher-android-arm64': 2.5.6 '@parcel/watcher-darwin-arm64': 2.5.6 @@ -5611,7 +5432,7 @@ snapshots: dependencies: '@types/estree': 1.0.9 estree-walker: 2.0.2 - picomatch: 4.0.4 + picomatch: 4.0.5 optionalDependencies: rollup: 2.80.0 @@ -5623,36 +5444,74 @@ snapshots: '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 + '@shikijs/core@4.3.1': + dependencies: + '@shikijs/primitive': 4.3.1 + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + '@shikijs/engine-javascript@4.3.0': dependencies: '@shikijs/types': 4.3.0 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.6 + '@shikijs/engine-javascript@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + '@shikijs/engine-oniguruma@4.3.0': dependencies: '@shikijs/types': 4.3.0 '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/engine-oniguruma@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/langs@4.3.0': dependencies: '@shikijs/types': 4.3.0 + '@shikijs/langs@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/primitive@4.3.0': dependencies: '@shikijs/types': 4.3.0 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + '@shikijs/primitive@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + '@shikijs/themes@4.3.0': dependencies: '@shikijs/types': 4.3.0 + '@shikijs/themes@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/types@4.3.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + '@shikijs/types@4.3.1': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + '@shikijs/vscode-textmate@10.0.2': {} '@surma/rollup-plugin-off-main-thread@2.2.3': @@ -5722,6 +5581,10 @@ snapshots: dependencies: undici-types: 7.18.2 + '@types/node@26.1.0': + dependencies: + undici-types: 8.3.0 + '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: '@types/react': 19.2.17 @@ -5734,7 +5597,7 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 24.13.2 + '@types/node': 26.1.0 '@types/trusted-types@2.0.7': {} @@ -5744,12 +5607,12 @@ snapshots: '@ungap/structured-clone@1.3.2': {} - '@vite-pwa/astro@1.2.0(astro@7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1))': + '@vite-pwa/astro@1.2.0(astro@7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1))': dependencies: - astro: 7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - vite-plugin-pwa: 1.3.0(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1) + astro: 7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite-plugin-pwa: 1.3.0(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1) - '@vitejs/plugin-react@5.2.0(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': + '@vitejs/plugin-react@5.2.0(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) @@ -5757,7 +5620,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -5782,14 +5645,14 @@ snapshots: path-browserify: 1.0.1 request-light: 0.7.0 vscode-languageserver: 9.0.1 - vscode-languageserver-protocol: 3.18.1 + vscode-languageserver-protocol: 3.18.2 vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.1.0 '@volar/language-service@2.4.28': dependencies: '@volar/language-core': 2.4.28 - vscode-languageserver-protocol: 3.18.1 + vscode-languageserver-protocol: 3.18.2 vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.1.0 @@ -5821,6 +5684,10 @@ snapshots: optionalDependencies: ajv: 8.20.0 + ajv-i18n@4.2.0(ajv@8.20.0): + dependencies: + ajv: 8.20.0 + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 @@ -5868,20 +5735,20 @@ snapshots: astring@1.9.0: {} - astro-expressive-code@0.44.0(astro@7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): + astro-expressive-code@0.44.0(astro@7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): dependencies: - astro: 7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + astro: 7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) rehype-expressive-code: 0.44.0 url-extras: 0.1.0 - astro@7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.2)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): + astro@7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): dependencies: - '@astrojs/compiler-rs': 0.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) - '@astrojs/internal-helpers': 0.10.0 - '@astrojs/markdown-satteri': 0.3.2 + '@astrojs/compiler-rs': 0.3.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2) + '@astrojs/internal-helpers': 0.10.1 + '@astrojs/markdown-satteri': 0.3.3 '@astrojs/telemetry': 3.3.2 '@capsizecss/unpack': 4.0.1 - '@clack/prompts': 1.6.0 + '@clack/prompts': 1.7.0 '@oslojs/encoding': 1.1.0 '@rollup/pluginutils': 5.4.0(rollup@2.80.0) am-i-vibing: 0.4.0 @@ -5894,7 +5761,7 @@ snapshots: devalue: 5.8.1 diff: 8.0.4 dset: 3.1.4 - es-module-lexer: 2.2.0 + es-module-lexer: 2.3.0 esbuild: 0.28.1 flattie: 1.1.1 fontace: 0.4.1 @@ -5910,13 +5777,12 @@ snapshots: neotraverse: 0.6.18 obug: 2.1.3 p-limit: 7.3.0 - p-queue: 9.3.0 - package-manager-detector: 1.6.0 + p-queue: 9.3.1 + package-manager-detector: 1.7.0 piccolore: 0.1.3 - picomatch: 4.0.4 - rehype: 13.0.2 + picomatch: 4.0.5 semver: 7.8.5 - shiki: 4.3.0 + shiki: 4.3.1 smol-toml: 1.7.0 svgo: 4.0.1 tinyclip: 0.1.15 @@ -5924,17 +5790,15 @@ snapshots: tinyglobby: 0.2.17 ultrahtml: 1.6.0 unifont: 0.7.4 - unist-util-visit: 5.1.0 unstorage: 1.17.5 - vfile: 6.0.3 - vite: 8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - vitefu: 1.1.3(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + vite: 8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vitefu: 1.1.3(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) xxhash-wasm: 1.1.0 yargs-parser: 22.0.0 zod: 4.4.3 optionalDependencies: - '@astrojs/markdown-remark': 7.2.0 - sharp: 0.34.5 + '@astrojs/markdown-remark': 7.2.1 + sharp: 0.35.3(@types/node@26.1.0) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -6014,7 +5878,7 @@ snapshots: bcp-47-match@2.0.3: {} - bcp-47@2.1.0: + bcp-47@2.1.1: dependencies: is-alphabetical: 2.0.1 is-alphanumerical: 2.0.1 @@ -6325,7 +6189,7 @@ snapshots: es-errors@1.3.0: {} - es-module-lexer@2.2.0: {} + es-module-lexer@2.3.0: {} es-object-atoms@1.1.2: dependencies: @@ -6460,9 +6324,9 @@ snapshots: dependencies: fast-string-width: 3.0.2 - fdir@6.5.0(picomatch@4.0.4): + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: - picomatch: 4.0.4 + picomatch: 4.0.5 filelist@1.0.6: dependencies: @@ -6795,13 +6659,13 @@ snapshots: http-cache-semantics@4.2.0: {} - i18next@26.3.3(typescript@6.0.3): + i18next@26.3.4(typescript@6.0.3): optionalDependencies: typescript: 6.0.3 idb@7.1.1: {} - immutable@5.1.8: {} + immutable@5.1.9: {} inflight@1.0.6: dependencies: @@ -7635,14 +7499,14 @@ snapshots: dependencies: yocto-queue: 1.2.2 - p-queue@9.3.0: + p-queue@9.3.1: dependencies: eventemitter3: 5.0.4 p-timeout: 7.0.1 p-timeout@7.0.1: {} - package-manager-detector@1.6.0: {} + package-manager-detector@1.7.0: {} pagefind@1.5.2: optionalDependencies: @@ -7691,6 +7555,8 @@ snapshots: picomatch@4.0.4: {} + picomatch@4.0.5: {} + possible-typed-array-names@1.1.0: {} postcss-nested@6.2.0(postcss@8.5.16): @@ -7709,7 +7575,7 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prettier@3.9.3: {} + prettier@3.9.4: {} pretty-bytes@5.6.0: {} @@ -8012,27 +7878,27 @@ snapshots: sass@1.101.0: dependencies: chokidar: 5.0.0 - immutable: 5.1.8 + immutable: 5.1.9 source-map-js: 1.2.1 optionalDependencies: '@parcel/watcher': 2.5.6 - satteri@0.9.3: + satteri@0.9.4: dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 '@types/mdast': 4.0.4 '@types/unist': 3.0.3 optionalDependencies: - '@bruits/satteri-darwin-arm64': 0.9.3 - '@bruits/satteri-darwin-x64': 0.9.3 - '@bruits/satteri-linux-arm64-gnu': 0.9.3 - '@bruits/satteri-linux-arm64-musl': 0.9.3 - '@bruits/satteri-linux-x64-gnu': 0.9.3 - '@bruits/satteri-linux-x64-musl': 0.9.3 - '@bruits/satteri-wasm32-wasi': 0.9.3 - '@bruits/satteri-win32-arm64-msvc': 0.9.3 - '@bruits/satteri-win32-x64-msvc': 0.9.3 + '@bruits/satteri-darwin-arm64': 0.9.4 + '@bruits/satteri-darwin-x64': 0.9.4 + '@bruits/satteri-linux-arm64-gnu': 0.9.4 + '@bruits/satteri-linux-arm64-musl': 0.9.4 + '@bruits/satteri-linux-x64-gnu': 0.9.4 + '@bruits/satteri-linux-x64-musl': 0.9.4 + '@bruits/satteri-wasm32-wasi': 0.9.4 + '@bruits/satteri-win32-arm64-msvc': 0.9.4 + '@bruits/satteri-win32-x64-msvc': 0.9.4 sax@1.6.0: {} @@ -8068,69 +7934,38 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.2 - sharp@0.34.5: - dependencies: - '@img/colour': 1.1.0 - detect-libc: 2.1.2 - semver: 7.8.5 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 - optional: true - - sharp@0.35.2: + sharp@0.35.3(@types/node@26.1.0): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.35.2 - '@img/sharp-darwin-x64': 0.35.2 - '@img/sharp-freebsd-wasm32': 0.35.2 - '@img/sharp-libvips-darwin-arm64': 1.3.1 - '@img/sharp-libvips-darwin-x64': 1.3.1 - '@img/sharp-libvips-linux-arm': 1.3.1 - '@img/sharp-libvips-linux-arm64': 1.3.1 - '@img/sharp-libvips-linux-ppc64': 1.3.1 - '@img/sharp-libvips-linux-riscv64': 1.3.1 - '@img/sharp-libvips-linux-s390x': 1.3.1 - '@img/sharp-libvips-linux-x64': 1.3.1 - '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 - '@img/sharp-libvips-linuxmusl-x64': 1.3.1 - '@img/sharp-linux-arm': 0.35.2 - '@img/sharp-linux-arm64': 0.35.2 - '@img/sharp-linux-ppc64': 0.35.2 - '@img/sharp-linux-riscv64': 0.35.2 - '@img/sharp-linux-s390x': 0.35.2 - '@img/sharp-linux-x64': 0.35.2 - '@img/sharp-linuxmusl-arm64': 0.35.2 - '@img/sharp-linuxmusl-x64': 0.35.2 - '@img/sharp-webcontainers-wasm32': 0.35.2 - '@img/sharp-win32-arm64': 0.35.2 - '@img/sharp-win32-ia32': 0.35.2 - '@img/sharp-win32-x64': 0.35.2 + '@img/sharp-darwin-arm64': 0.35.3 + '@img/sharp-darwin-x64': 0.35.3 + '@img/sharp-freebsd-wasm32': 0.35.3 + '@img/sharp-libvips-darwin-arm64': 1.3.2 + '@img/sharp-libvips-darwin-x64': 1.3.2 + '@img/sharp-libvips-linux-arm': 1.3.2 + '@img/sharp-libvips-linux-arm64': 1.3.2 + '@img/sharp-libvips-linux-ppc64': 1.3.2 + '@img/sharp-libvips-linux-riscv64': 1.3.2 + '@img/sharp-libvips-linux-s390x': 1.3.2 + '@img/sharp-libvips-linux-x64': 1.3.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + '@img/sharp-linux-arm': 0.35.3 + '@img/sharp-linux-arm64': 0.35.3 + '@img/sharp-linux-ppc64': 0.35.3 + '@img/sharp-linux-riscv64': 0.35.3 + '@img/sharp-linux-s390x': 0.35.3 + '@img/sharp-linux-x64': 0.35.3 + '@img/sharp-linuxmusl-arm64': 0.35.3 + '@img/sharp-linuxmusl-x64': 0.35.3 + '@img/sharp-webcontainers-wasm32': 0.35.3 + '@img/sharp-win32-arm64': 0.35.3 + '@img/sharp-win32-ia32': 0.35.3 + '@img/sharp-win32-x64': 0.35.3 + '@types/node': 26.1.0 shiki@4.3.0: dependencies: @@ -8143,6 +7978,17 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + shiki@4.3.1: + dependencies: + '@shikijs/core': 4.3.1 + '@shikijs/engine-javascript': 4.3.1 + '@shikijs/engine-oniguruma': 4.3.1 + '@shikijs/langs': 4.3.1 + '@shikijs/themes': 4.3.1 + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 @@ -8317,8 +8163,8 @@ snapshots: tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 tr46@1.0.1: dependencies: @@ -8389,6 +8235,8 @@ snapshots: undici-types@7.18.2: {} + undici-types@8.3.0: {} + unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-match-property-ecmascript@2.0.0: @@ -8506,18 +8354,18 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-pwa@1.3.0(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1): + vite-plugin-pwa@1.3.0(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1): dependencies: debug: 4.4.3 pretty-bytes: 6.1.1 tinyglobby: 0.2.17 - vite: 8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) workbox-build: 7.3.0(@types/babel__core@7.20.5) workbox-window: 7.4.1 transitivePeerDependencies: - supports-color - vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): + vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -8525,18 +8373,18 @@ snapshots: rolldown: 1.1.3 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 24.13.2 + '@types/node': 26.1.0 esbuild: 0.28.1 fsevents: 2.3.3 sass: 1.101.0 terser: 5.48.0 yaml: 2.9.0 - vitefu@1.1.3(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): + vitefu@1.1.3(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): optionalDependencies: - vite: 8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - volar-service-css@0.0.70(@volar/language-service@2.4.28): + volar-service-css@0.0.71(@volar/language-service@2.4.28): dependencies: vscode-css-languageservice: 6.3.10 vscode-languageserver-textdocument: 1.0.12 @@ -8544,7 +8392,7 @@ snapshots: optionalDependencies: '@volar/language-service': 2.4.28 - volar-service-emmet@0.0.70(@volar/language-service@2.4.28): + volar-service-emmet@0.0.71(@volar/language-service@2.4.28): dependencies: '@emmetio/css-parser': 0.4.1 '@emmetio/html-matcher': 1.3.0 @@ -8553,7 +8401,7 @@ snapshots: optionalDependencies: '@volar/language-service': 2.4.28 - volar-service-html@0.0.70(@volar/language-service@2.4.28): + volar-service-html@0.0.71(@volar/language-service@2.4.28): dependencies: vscode-html-languageservice: 5.6.2 vscode-languageserver-textdocument: 1.0.12 @@ -8561,20 +8409,20 @@ snapshots: optionalDependencies: '@volar/language-service': 2.4.28 - volar-service-prettier@0.0.70(@volar/language-service@2.4.28)(prettier@3.9.3): + volar-service-prettier@0.0.71(@volar/language-service@2.4.28)(prettier@3.9.4): dependencies: vscode-uri: 3.1.0 optionalDependencies: '@volar/language-service': 2.4.28 - prettier: 3.9.3 + prettier: 3.9.4 - volar-service-typescript-twoslash-queries@0.0.70(@volar/language-service@2.4.28): + volar-service-typescript-twoslash-queries@0.0.71(@volar/language-service@2.4.28): dependencies: vscode-uri: 3.1.0 optionalDependencies: '@volar/language-service': 2.4.28 - volar-service-typescript@0.0.70(@volar/language-service@2.4.28): + volar-service-typescript@0.0.71(@volar/language-service@2.4.28): dependencies: path-browserify: 1.0.1 semver: 7.8.5 @@ -8585,10 +8433,10 @@ snapshots: optionalDependencies: '@volar/language-service': 2.4.28 - volar-service-yaml@0.0.70(@volar/language-service@2.4.28): + volar-service-yaml@0.0.71(@volar/language-service@2.4.28): dependencies: vscode-uri: 3.1.0 - yaml-language-server: 1.20.0 + yaml-language-server: 1.23.0 optionalDependencies: '@volar/language-service': 2.4.28 @@ -8616,16 +8464,16 @@ snapshots: vscode-jsonrpc@8.2.0: {} - vscode-jsonrpc@9.0.0: {} + vscode-jsonrpc@9.0.1: {} vscode-languageserver-protocol@3.17.5: dependencies: vscode-jsonrpc: 8.2.0 vscode-languageserver-types: 3.17.5 - vscode-languageserver-protocol@3.18.1: + vscode-languageserver-protocol@3.18.2: dependencies: - vscode-jsonrpc: 9.0.0 + vscode-jsonrpc: 9.0.1 vscode-languageserver-types: 3.18.0 vscode-languageserver-textdocument@1.0.12: {} @@ -8829,21 +8677,22 @@ snapshots: yallist@3.1.1: {} - yaml-language-server@1.20.0: + yaml-language-server@1.23.0: dependencies: '@vscode/l10n': 0.0.18 ajv: 8.20.0 ajv-draft-04: 1.0.0(ajv@8.20.0) - prettier: 3.9.3 + ajv-i18n: 4.2.0(ajv@8.20.0) + prettier: 3.9.4 request-light: 0.5.8 vscode-json-languageservice: 4.1.8 vscode-languageserver: 9.0.1 vscode-languageserver-textdocument: 1.0.12 vscode-languageserver-types: 3.18.0 vscode-uri: 3.1.0 - yaml: 2.7.1 + yaml: 2.8.3 - yaml@2.7.1: {} + yaml@2.8.3: {} yaml@2.9.0: {} From 27d7efb4813a0275565e1902167891f3f8abc6d2 Mon Sep 17 00:00:00 2001 From: Linwood CI Date: Mon, 6 Jul 2026 13:43:48 +0000 Subject: [PATCH 030/117] Add changelog of v2.6.0-beta.1 --- CHANGELOG.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96a3a703ac95..98bd4a6cf8df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,28 @@ # Changelog - + + +## 2.6.0-beta.1 (2026-07-06) + +* Add pages selector with range input ([#1151](https://github.com/LinwoodDev/Butterfly/issues/1151)) +* Add internal page numbers to the pages navigator ([#1143](https://github.com/LinwoodDev/Butterfly/issues/1143)) +* Add cross-page area selection and deletion ([#1143](https://github.com/LinwoodDev/Butterfly/issues/1143)) +* Add apply areas option to templates ([#1151](https://github.com/LinwoodDev/Butterfly/issues/1151)) +* Add combine paths option ([#1071](https://github.com/LinwoodDev/Butterfly/issues/1071)) +* Add xournal++ exporter +* Add next and previous page shortcuts +* Improve xournal++ importer +* Improve state management for better linking different systems together +* Improve window title bar design +* Unify area context menu and area selection context menu ([#1151](https://github.com/LinwoodDev/Butterfly/issues/1151)) +* Fix refresh foregrounds can be run concurrently +* Fix location synchronization issues +* Fix saved documents being saved again +* Fix imported documents starting as saved +* Fix zoom slider and reset button not working if zoom is locked +* Fix polygon disappears + +Read more here: https://linwood.dev/butterfly/2.6.0-beta.1 ## 2.6.0-beta.0 (2026-06-22) From b20e3f4223bc14b60c36ff4d799ab74540a5b282 Mon Sep 17 00:00:00 2001 From: Linwood CI Date: Mon, 6 Jul 2026 13:52:19 +0000 Subject: [PATCH 031/117] Update Version to 2.6.0-beta.2 --- api/pubspec.yaml | 2 +- app/linux/debian/DEBIAN/control | 2 +- app/pubspec.lock | 2 +- app/pubspec.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/pubspec.yaml b/api/pubspec.yaml index d9b2775169d7..163a55e9b25e 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -1,6 +1,6 @@ name: butterfly_api description: The Linwood Butterfly API -version: 2.6.0-beta.1 +version: 2.6.0-beta.2 publish_to: none environment: diff --git a/app/linux/debian/DEBIAN/control b/app/linux/debian/DEBIAN/control index 39330d49f110..4f884737660e 100644 --- a/app/linux/debian/DEBIAN/control +++ b/app/linux/debian/DEBIAN/control @@ -1,5 +1,5 @@ Package: linwood-butterfly -Version: 2.6.0-beta.1 +Version: 2.6.0-beta.2 Section: base Priority: optional Homepage: https://github.com/LinwoodDev/butterfly diff --git a/app/pubspec.lock b/app/pubspec.lock index 112c99b58d66..d5f03fd63e73 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -151,7 +151,7 @@ packages: path: "../api" relative: true source: path - version: "2.6.0-beta.1" + version: "2.6.0-beta.2" camera: dependency: "direct main" description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 707aa8f5da83..7873f2b8dadc 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -13,7 +13,7 @@ publish_to: none # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 2.6.0-beta.1+187 +version: 2.6.0-beta.2+188 environment: sdk: ">=3.12.2 <4.0.0" From 51497059c63e7ad2ddf419a14ec437239d063ecd Mon Sep 17 00:00:00 2001 From: Linwood CI Date: Mon, 6 Jul 2026 13:58:38 +0000 Subject: [PATCH 032/117] Bump version --- app/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 045edbe889ae..b4359710a576 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -13,7 +13,7 @@ publish_to: none # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 2.5.3+187 +version: 2.5.3+188 environment: sdk: ">=3.9.0 <4.0.0" From e03e5534fa5e534b750ce21a33510c54d8a52d7a Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 30 Jun 2026 00:36:55 +0200 Subject: [PATCH 033/117] Refactor whole state structure (split currentindexcubit), add persistent document states --- app/integration_test/screenshot.dart | 40 +- app/lib/actions/change_path.dart | 12 +- app/lib/actions/change_tool.dart | 4 +- app/lib/actions/export.dart | 5 +- app/lib/actions/hide_ui.dart | 4 +- app/lib/actions/pdf_export.dart | 4 +- app/lib/actions/save.dart | 12 +- app/lib/actions/select.dart | 4 +- app/lib/actions/zoom.dart | 7 +- app/lib/api/file_system.dart | 50 +- app/lib/bloc/document_bloc.dart | 194 +-- app/lib/bloc/document_state.dart | 2 +- app/lib/cubits/current_index.freezed.dart | 357 ------ app/lib/cubits/editor_controller.dart | 145 +++ ...ex.dart => editor_controller_methods.dart} | 1112 ++++++----------- app/lib/cubits/editor_runtime.dart | 551 ++++++++ app/lib/cubits/editor_runtime.freezed.dart | 831 ++++++++++++ app/lib/cubits/editor_session.dart | 220 ++++ app/lib/dialogs/area/context.dart | 10 +- app/lib/dialogs/collaboration/dialog.dart | 18 +- app/lib/dialogs/collaboration/view.dart | 8 +- app/lib/dialogs/collections.dart | 4 +- app/lib/dialogs/elements.dart | 7 +- app/lib/dialogs/export/general.dart | 9 +- app/lib/dialogs/export/pdf.dart | 21 +- app/lib/dialogs/export/thumbnail.dart | 13 +- app/lib/dialogs/import/add.dart | 6 +- app/lib/dialogs/packs/asset.dart | 3 +- app/lib/embed/handler.dart | 23 +- app/lib/handlers/area.dart | 13 +- app/lib/handlers/barcode.dart | 2 +- app/lib/handlers/eraser.dart | 6 +- app/lib/handlers/eye_dropper.dart | 6 +- app/lib/handlers/grid.dart | 2 +- app/lib/handlers/handler.dart | 22 +- app/lib/handlers/import.dart | 2 +- app/lib/handlers/label.dart | 8 +- app/lib/handlers/laser.dart | 13 +- app/lib/handlers/mixins.dart | 24 +- app/lib/handlers/pen.dart | 13 +- app/lib/handlers/polygon.dart | 10 +- app/lib/handlers/presentation.dart | 8 +- app/lib/handlers/ruler.dart | 6 +- app/lib/handlers/select.dart | 20 +- app/lib/handlers/shape.dart | 4 +- app/lib/handlers/spacer.dart | 2 +- app/lib/handlers/stamp.dart | 4 +- app/lib/handlers/texture.dart | 2 +- app/lib/models/persisted_document_state.dart | 114 ++ .../persisted_document_state.freezed.dart | 530 ++++++++ .../models/persisted_document_state.g.dart | 95 ++ app/lib/models/viewport.dart | 2 +- app/lib/renderers/elements/image.dart | 4 +- app/lib/renderers/elements/pdf.dart | 6 +- app/lib/renderers/elements/pen.dart | 2 +- app/lib/renderers/elements/polygon.dart | 4 +- app/lib/renderers/elements/text.dart | 6 +- app/lib/renderers/renderer.dart | 8 +- app/lib/selections/document.dart | 27 +- app/lib/selections/selection.dart | 4 +- app/lib/services/export.dart | 4 +- app/lib/services/import.dart | 30 +- app/lib/services/network.dart | 3 +- app/lib/settings/data.dart | 4 +- app/lib/view_painter.dart | 2 +- app/lib/views/app_bar.dart | 679 +++++----- app/lib/views/edit.dart | 83 +- app/lib/views/main.dart | 369 ++++-- app/lib/views/navigator/areas.dart | 51 +- app/lib/views/navigator/components.dart | 10 +- app/lib/views/navigator/files.dart | 10 +- app/lib/views/navigator/view.dart | 32 +- app/lib/views/pen_only_toggle.dart | 129 +- app/lib/views/property.dart | 12 +- app/lib/views/toolbar/polygon.dart | 4 +- app/lib/views/toolbar/view.dart | 9 +- app/lib/views/view.dart | 635 +++++----- app/lib/views/zoom.dart | 21 +- app/lib/widgets/search.dart | 5 +- app/pubspec.lock | 2 +- app/pubspec.yaml | 1 + app/test/bloc/document_bloc_test.dart | 153 +-- app/test/cubits/editor_session_test.dart | 165 +++ app/test/handlers/polygon_handler_test.dart | 12 +- app/test/handlers/shape_handler_test.dart | 8 +- app/test/helpers/mocks.dart | 19 + app/test/renderers/image_renderer_test.dart | 6 +- app/test/views/navigator/layers_test.dart | 8 +- app/test/views/navigator/pages_test.dart | 8 +- .../views/project_page_lifecycle_test.dart | 50 +- 90 files changed, 4695 insertions(+), 2474 deletions(-) delete mode 100644 app/lib/cubits/current_index.freezed.dart create mode 100644 app/lib/cubits/editor_controller.dart rename app/lib/cubits/{current_index.dart => editor_controller_methods.dart} (70%) create mode 100644 app/lib/cubits/editor_runtime.dart create mode 100644 app/lib/cubits/editor_runtime.freezed.dart create mode 100644 app/lib/cubits/editor_session.dart create mode 100644 app/lib/models/persisted_document_state.dart create mode 100644 app/lib/models/persisted_document_state.freezed.dart create mode 100644 app/lib/models/persisted_document_state.g.dart create mode 100644 app/test/cubits/editor_session_test.dart diff --git a/app/integration_test/screenshot.dart b/app/integration_test/screenshot.dart index ae75db933b44..c4c030f43cf9 100644 --- a/app/integration_test/screenshot.dart +++ b/app/integration_test/screenshot.dart @@ -3,7 +3,7 @@ import 'dart:ui' as ui; import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/dialogs/import/add.dart'; @@ -223,15 +223,17 @@ void main() { await pumpDocument(tester, size); var viewportContext = tester.element(find.byType(MainViewViewport)); - var currentIndexCubit = viewportContext.read(); + var editorController = viewportContext.read(); var transformCubit = viewportContext.read(); var bloc = viewportContext.read(); var pen = bloc.state.info?.tools.whereType().firstOrNull; final isDesktop = size.width >= 840; if (isDesktop) { - currentIndexCubit.setNavigatorPage(NavigatorPage.pages); - currentIndexCubit.setNavigatorEnabled(true); + editorController.viewCubit.setNavigator( + page: NavigatorPage.pages, + enabled: true, + ); await settle(tester); } await frameOpeningDocument(tester, size, transformCubit); @@ -246,10 +248,12 @@ void main() { builder: (context) => MultiBlocProvider( providers: [ BlocProvider.value(value: bloc), - BlocProvider.value(value: currentIndexCubit), BlocProvider.value(value: transformCubit), ], - child: const TemplateDialog(), + child: RepositoryProvider.value( + value: editorController, + child: const TemplateDialog(), + ), ), ); await tester.pumpAndSettle(); @@ -269,7 +273,7 @@ void main() { await pumpDocument(tester, size, documentName: propertiesDocumentName); viewportContext = tester.element(find.byType(MainViewViewport)); - currentIndexCubit = viewportContext.read(); + editorController = viewportContext.read(); transformCubit = viewportContext.read(); bloc = viewportContext.read(); pen = bloc.state.info?.tools.whereType().firstOrNull; @@ -279,35 +283,35 @@ void main() { ); } await frameOpeningDocument(tester, size, transformCubit); - currentIndexCubit.setNavigatorEnabled(false); - currentIndexCubit.changeSelection(pen, false); + editorController.viewCubit.setNavigator(enabled: false); + editorController.toolCubit.changeSelection(pen, false); await settle(tester); expect(find.byType(PropertyView), findsOneWidget); await takeScreenshot(tester, '$directory/4-properties'); await pumpDocument(tester, size); viewportContext = tester.element(find.byType(MainViewViewport)); - currentIndexCubit = viewportContext.read(); + editorController = viewportContext.read(); transformCubit = viewportContext.read(); bloc = viewportContext.read(); await frameOpeningDocument(tester, size, transformCubit); - currentIndexCubit.changeSelection(currentIndexCubit, false); + editorController.toolCubit.changeSelection(editorController, false); await settle(tester); await takeScreenshot(tester, '$directory/5-tools'); - currentIndexCubit.resetSelection(force: true); + editorController.toolCubit.resetSelection(force: true); await settle(tester); final importService = viewportContext.read(); showGeneralDialog( context: viewportContext, pageBuilder: (context, _, _) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: bloc), - BlocProvider.value(value: currentIndexCubit), - ], - child: RepositoryProvider.value( - value: importService, + providers: [BlocProvider.value(value: bloc)], + child: MultiRepositoryProvider( + providers: [ + RepositoryProvider.value(value: editorController), + RepositoryProvider.value(value: importService), + ], child: const AddDialog(), ), ), diff --git a/app/lib/actions/change_path.dart b/app/lib/actions/change_path.dart index d4a9bbd3e27e..bcacd3b99283 100644 --- a/app/lib/actions/change_path.dart +++ b/app/lib/actions/change_path.dart @@ -27,10 +27,9 @@ class ChangePathAction extends Action { @override Future invoke(ChangePathIntent intent) async { final bloc = context.read(); - final cubit = bloc.currentIndexCubit; - final cubitState = cubit.state; - if (cubitState.location.path == '') return; - final location = cubitState.location; + final cubit = bloc.editorController; + final location = cubit.saveCubit.state.location; + if (location.path == '') return; final settings = context.read().state; final fileSystem = context.read().buildDocumentSystem( settings.getRemote(location.remote), @@ -46,7 +45,10 @@ class ChangePathAction extends Action { ), ); if (newLocations == null) return; - cubit.setSaveState(location: newLocations.first, isCreating: false); + cubit.saveCubit.setSaveState( + location: newLocations.first, + isCreating: false, + ); bloc.save(); } } diff --git a/app/lib/actions/change_tool.dart b/app/lib/actions/change_tool.dart index 5d868c279f4c..4e2a4dc7a46b 100644 --- a/app/lib/actions/change_tool.dart +++ b/app/lib/actions/change_tool.dart @@ -1,4 +1,4 @@ -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -40,7 +40,7 @@ class ChangeToolAction extends Action { @override Future invoke(ChangeToolIntent intent) async { final bloc = context.read(); - context.read().changeTool( + context.read().changeTool( bloc, context: context, index: intent.index, diff --git a/app/lib/actions/export.dart b/app/lib/actions/export.dart index 68dabb47a84e..1ac4b9927a5e 100644 --- a/app/lib/actions/export.dart +++ b/app/lib/actions/export.dart @@ -37,10 +37,7 @@ class ExportAction extends Action { final bloc = context.read(); final state = bloc.state; if (state is! DocumentLoaded) return; - final data = await state.saveData( - null, - bloc.currentIndexCubit.state.viewOption, - ); + final data = await state.saveData(); exportData(context, data, isTextBased: intent.isText); } } diff --git a/app/lib/actions/hide_ui.dart b/app/lib/actions/hide_ui.dart index 8919cdb10c96..77f14fda688a 100644 --- a/app/lib/actions/hide_ui.dart +++ b/app/lib/actions/hide_ui.dart @@ -1,4 +1,4 @@ -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -21,6 +21,6 @@ class HideUIAction extends Action { @override void invoke(HideUIIntent intent) { - context.read().toggleKeyboardHideUI(); + context.read().toggleKeyboardHideUI(); } } diff --git a/app/lib/actions/pdf_export.dart b/app/lib/actions/pdf_export.dart index 3a5d59ee75af..71f8b199a341 100644 --- a/app/lib/actions/pdf_export.dart +++ b/app/lib/actions/pdf_export.dart @@ -32,7 +32,9 @@ class PdfExportAction extends Action { final state = bloc.state; if (state is! DocumentLoadSuccess) return; var areas = [ - state.areaPreset(bloc.currentIndexCubit.state.cameraViewport), + state.areaPreset( + bloc.editorController.rendererCubit.state.cameraViewport, + ), ]; if (state.info.exportPresets.isNotEmpty) { final preset = await showDialog( diff --git a/app/lib/actions/save.dart b/app/lib/actions/save.dart index 6ba7461251bc..553d639cbfee 100644 --- a/app/lib/actions/save.dart +++ b/app/lib/actions/save.dart @@ -4,7 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:keybinder/keybinder.dart'; import '../bloc/document_bloc.dart'; -import '../cubits/current_index.dart'; +import '../cubits/editor_controller.dart'; import '../embed/action.dart'; class SaveIntent extends Intent { @@ -27,13 +27,9 @@ class SaveAction extends Action { final bloc = context.read(); final state = bloc.state; if (state is! DocumentLoadSuccess) return; - final currentIndex = bloc.currentIndexCubit.state; - if (currentIndex.embedding?.save ?? false) { - sendEmbedMessage( - 'save', - (await state.saveData(null, currentIndex.viewOption)).exportAsBytes(), - ); - bloc.currentIndexCubit.setSaveState(saved: SaveState.saved); + if (bloc.editorController.saveCubit.state.embedding?.save ?? false) { + sendEmbedMessage('save', (await state.saveData()).exportAsBytes()); + bloc.editorController.saveCubit.setSaveState(saved: SaveState.saved); } else { await bloc.save(force: true); } diff --git a/app/lib/actions/select.dart b/app/lib/actions/select.dart index b6af8bb672b6..68a72864ac5f 100644 --- a/app/lib/actions/select.dart +++ b/app/lib/actions/select.dart @@ -7,7 +7,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:keybinder/keybinder.dart'; -import '../cubits/current_index.dart'; +import '../cubits/editor_controller.dart'; class SelectAllIntent extends Intent { const SelectAllIntent(); @@ -26,7 +26,7 @@ class SelectAllAction extends Action { @override Future invoke(SelectAllIntent intent) async { - final cubit = context.read(); + final cubit = context.read(); if (cubit.getHandler() is SelectHandler) return; final bloc = context.read(); final handler = await cubit.changeTemporaryHandler( diff --git a/app/lib/actions/zoom.dart b/app/lib/actions/zoom.dart index b2151e5e177e..13ed0c33bc11 100644 --- a/app/lib/actions/zoom.dart +++ b/app/lib/actions/zoom.dart @@ -1,4 +1,4 @@ -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -29,9 +29,8 @@ class ZoomAction extends Action { @override void invoke(ZoomIntent intent) { - final cubit = context.read(); - final currentIndex = cubit.state; - final viewport = currentIndex.cameraViewport; + final cubit = context.read(); + final viewport = cubit.rendererCubit.state.cameraViewport; final center = Offset( (viewport.width ?? 0) / 2, (viewport.height ?? 0) / 2, diff --git a/app/lib/api/file_system.dart b/app/lib/api/file_system.dart index 3b795bccaed1..73a7fefe83ac 100644 --- a/app/lib/api/file_system.dart +++ b/app/lib/api/file_system.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:archive/archive.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/models/defaults.dart'; +import 'package:butterfly/models/persisted_document_state.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:butterfly_api/butterfly_text.dart' as text; import 'package:flutter/foundation.dart'; @@ -77,6 +78,7 @@ Future getButterflyDocumentsDirectory([ExternalStorage? storage]) => typedef DocumentFileSystem = TypedDirectoryFileSystem; typedef TemplateFileSystem = TypedKeyFileSystem; typedef PackFileSystem = TypedKeyFileSystem; +typedef DocumentStateFileSystem = TypedKeyFileSystem; const kCorePackFileName = 'Core.bfly'; @@ -109,11 +111,15 @@ class ButterflyFileSystem { // ignore: unused_field final BuildContext _context; final SettingsCubit settingsCubit; - final FileSystemConfig _documentConfig, _templateConfig, _packConfig; + final FileSystemConfig _documentConfig, + _templateConfig, + _packConfig, + _documentStateConfig; final _documentCache = {}; final _templateCache = {}; final _packCache = {}; + final _documentStateCache = {}; StreamSubscription? _settingsSubscription; ButterflyFileSystem(this._context, this.settingsCubit) @@ -145,6 +151,15 @@ class ButterflyFileSystem { databaseVersion: _databaseVersion, onDatabaseUpgrade: _upgradeDatabase, defaultStorageKey: 'defaultPack', + ), + _documentStateConfig = FileSystemConfig( + passwordStorage: passwordStorage, + storeName: 'documentstates', + variant: 'documentstates', + getDirectory: _getRemoteDirectory('DocumentStates'), + database: _database, + databaseVersion: _databaseVersion, + onDatabaseUpgrade: _upgradeDatabase, ) { _listenSettings(); } @@ -172,13 +187,14 @@ class ButterflyFileSystem { _documentCache.clear(); _templateCache.clear(); _packCache.clear(); + _documentStateCache.clear(); } factory ButterflyFileSystem.build(BuildContext context) => ButterflyFileSystem(context, context.read()); static const _database = 'butterfly.db'; - static const _databaseVersion = 4; + static const _databaseVersion = 5; static Future _upgradeDatabase(VersionChangeEvent event) async { final db = event.database; @@ -232,6 +248,10 @@ class ButterflyFileSystem { ); await txn.completed; } + if (event.oldVersion < 5) { + db.createObjectStore('documentstates'); + db.createObjectStore('documentstates-data'); + } } String _cacheKey(ExternalStorage? storage) => storage?.identifier ?? 'local'; @@ -304,6 +324,26 @@ class ButterflyFileSystem { return system; } + DocumentStateFileSystem buildDocumentStateSystem([ + ExternalStorage? storage, + bool forceRecreate = false, + ]) { + final key = _cacheKey(storage); + if (!forceRecreate) { + final cached = _documentStateCache[key]; + if (cached != null) return cached; + } + final system = TypedKeyFileSystem.build( + _documentStateConfig, + onEncode: encodePersistedDocumentState, + onDecode: decodePersistedDocumentState, + storage: _cacheAllStorage(storage, _documentStateConfig.variant), + useAndroidSaf: true, + ); + _documentStateCache[key] = system; + return system; + } + DocumentFileSystem buildDefaultDocumentSystem({bool forceRecreate = false}) => buildDocumentSystem( settingsCubit.state.getDefaultRemote(), @@ -411,9 +451,15 @@ class ButterflyFileSystem { _packCache.remove(key); } + void removeCachedDocumentStateSystem(ExternalStorage? storage) { + final key = _cacheKey(storage); + _documentStateCache.remove(key); + } + void removeCachedFileSystem(ExternalStorage? storage) { removeCachedDocumentSystem(storage); removeCachedTemplateSystem(storage); removeCachedPackSystem(storage); + removeCachedDocumentStateSystem(storage); } } diff --git a/app/lib/bloc/document_bloc.dart b/app/lib/bloc/document_bloc.dart index 21e4e92701d8..cf627c9e3650 100644 --- a/app/lib/bloc/document_bloc.dart +++ b/app/lib/bloc/document_bloc.dart @@ -5,7 +5,7 @@ import 'dart:math'; import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:butterfly/api/file_system.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/handlers/handler.dart'; import 'package:butterfly/helpers/async.dart'; import 'package:butterfly/helpers/rect.dart'; @@ -145,18 +145,18 @@ class DocumentBloc extends ReplayBloc { final _historyReloadRunner = CoalescedAsyncRunner( delay: const Duration(milliseconds: 50), ); - CurrentIndexCubit? _currentIndexCubit; + EditorController? _editorController; AssetService? _assetService; - CurrentIndexCubit get currentIndexCubit => _currentIndexCubit!; - TransformCubit get transformCubit => currentIndexCubit.transformCubit; + EditorController get editorController => _editorController!; + TransformCubit get transformCubit => editorController.transformCubit; NetworkingService? get networkingService => - _currentIndexCubit?.networkingService; - Embedding? get embedding => _currentIndexCubit?.state.embedding; + _editorController?.networkingService; + Embedding? get embedding => _editorController?.saveCubit.state.embedding; factory DocumentBloc( ButterflyFileSystem fileSystem, - CurrentIndexCubit currentIndexCubit, + EditorController editorController, WindowCubit windowCubit, NoteData initial, AssetLocation location, [ @@ -164,31 +164,43 @@ class DocumentBloc extends ReplayBloc { DocumentPage? page, String? pageName, bool absolute = false, + String? currentLayer, + String? currentCollection, + Set? invisibleLayers, ]) { final resolvedAssetService = assetService ?? AssetService(); - currentIndexCubit.setSaveState(location: location, absolute: absolute); + editorController.saveCubit.setSaveState( + location: location, + absolute: absolute, + ); return DocumentBloc._( fileSystem, - currentIndexCubit, + editorController, windowCubit, initial, resolvedAssetService, page, pageName, absolute, + currentLayer, + currentCollection, + invisibleLayers, ); } DocumentBloc._( ButterflyFileSystem fileSystem, - CurrentIndexCubit currentIndexCubit, + EditorController editorController, WindowCubit windowCubit, NoteData initial, AssetService assetService, [ DocumentPage? page, String? pageName, bool absolute = false, - ]) : _currentIndexCubit = currentIndexCubit, + String? currentLayer, + String? currentCollection, + Set? invisibleLayers, + ]) : _editorController = editorController, _assetService = assetService, super( DocumentLoadSuccess( @@ -199,6 +211,9 @@ class DocumentBloc extends ReplayBloc { absolute: absolute, fileSystem: fileSystem, pageName: pageName ?? initial.getPages(true).firstOrNull ?? '', + currentLayer: currentLayer, + currentCollection: currentCollection ?? '', + invisibleLayers: invisibleLayers ?? const {}, ), ) { _init(); @@ -206,11 +221,11 @@ class DocumentBloc extends ReplayBloc { DocumentBloc.error( ButterflyFileSystem fileSystem, - CurrentIndexCubit currentIndexCubit, + EditorController editorController, WindowCubit windowCubit, String message, [ StackTrace? stackTrace, - ]) : _currentIndexCubit = currentIndexCubit, + ]) : _editorController = editorController, _assetService = null, super( DocumentLoadFailure( @@ -223,9 +238,9 @@ class DocumentBloc extends ReplayBloc { DocumentBloc.placeholder( ButterflyFileSystem fileSystem, - CurrentIndexCubit currentIndexCubit, + EditorController editorController, WindowCubit windowCubit, - ) : _currentIndexCubit = currentIndexCubit, + ) : _editorController = editorController, _assetService = null, super( DocumentLoadFailure( @@ -310,6 +325,7 @@ class DocumentBloc extends ReplayBloc { ), reset: true, ); + editorController.editorSessionCubit?.updatePage(event.pageName); }); on((event, emit) { final current = state; @@ -413,7 +429,7 @@ class DocumentBloc extends ReplayBloc { ), ), addedElements: renderers, - shouldRefresh: () => currentIndexCubit.getHandler().onRenderersCreated( + shouldRefresh: () => editorController.getHandler().onRenderersCreated( current.page, renderers, ), @@ -423,11 +439,11 @@ class DocumentBloc extends ReplayBloc { final current = state; if (current is! DocumentLoadSuccess) return; if (!(embedding?.editable ?? true)) return; - final cubit = currentIndexCubit; + final cubit = editorController; final renderers = >[]; - final selected = cubit.state.selection?.selected.toList(); + final selected = cubit.toolCubit.state.selection?.selected.toList(); final page = current.page; - final oldRenderers = cubit.renderers; + final oldRenderers = cubit.rendererCubit.renderers; var data = current.data; final imported = {}; final elements = >{}; @@ -496,7 +512,7 @@ class DocumentBloc extends ReplayBloc { } data = data.removeAssets(unusedAssets.toList()); - cubit.changeSelection(Selection.fromList(selected), false); + cubit.toolCubit.changeSelection(Selection.fromList(selected), false); return _saveState( emit, state: current.copyWith(page: newPage, data: data), @@ -517,7 +533,7 @@ class DocumentBloc extends ReplayBloc { final current = state; if (current is! DocumentLoadSuccess) return; final renderers = List>.from( - currentIndexCubit.renderers, + editorController.rendererCubit.renderers, ); void insertElement( List content, @@ -628,9 +644,9 @@ class DocumentBloc extends ReplayBloc { .toList(), ), ); - currentIndexCubit.removeSelection(event.elements); + editorController.toolCubit.removeSelection(event.elements); final unusedAssets = {}; - final removedRenderers = currentIndexCubit.renderers + final removedRenderers = editorController.rendererCubit.renderers .where((e) => event.elements.contains(e.element.id)) .toList(); for (final renderer in removedRenderers) { @@ -649,7 +665,7 @@ class DocumentBloc extends ReplayBloc { return _saveState( emit, state: current.copyWith(page: newPage, data: data), - replacedElements: currentIndexCubit.renderers + replacedElements: editorController.rendererCubit.renderers .where((e) => !event.elements.contains(e.element.id)) .toList(), ); @@ -686,7 +702,7 @@ class DocumentBloc extends ReplayBloc { final current = state; if (current is! DocumentLoadSuccess) return; if (!(embedding?.editable ?? true)) return; - var selection = currentIndexCubit.state.selection; + var selection = editorController.toolCubit.state.selection; final changedTools = event.tools .map((e) => e.copyWith(id: e.id ?? createUniqueId())) .toList(); @@ -719,19 +735,21 @@ class DocumentBloc extends ReplayBloc { return tool; } selection = _updateSelection(selection, tool, updated); - final currentTool = currentIndexCubit.state.handler.data; + final currentTool = editorController.toolCubit.state.handler.data; if (currentTool is Tool && (currentTool.id != null && currentTool.id == tool.id || identical(currentTool, tool) || - currentIndexCubit.state.index == index)) { + editorController.toolCubit.state.index == index)) { updatedCurrent = updated; } - final tempHandler = currentIndexCubit.state.temporaryHandler; + final tempHandler = + editorController.toolCubit.state.temporaryHandler; if (tempHandler != null && (tempHandler.data.id != null && tempHandler.data.id == tool.id || identical(tempHandler.data, tool) || - currentIndexCubit.state.temporaryIndex == index)) { + editorController.toolCubit.state.temporaryIndex == + index)) { updatedTemporary = updated; } return updated; @@ -740,14 +758,14 @@ class DocumentBloc extends ReplayBloc { ), ); if (updatedCurrent != null) { - currentIndexCubit.updateTool(this, updatedCurrent!); + editorController.updateTool(this, updatedCurrent!); } - currentIndexCubit.updateTogglingTools(this, changedTools); + editorController.updateTogglingTools(this, changedTools); if (updatedTemporary != null) { - currentIndexCubit.updateTemporaryTool(this, updatedTemporary!); + editorController.updateTemporaryTool(this, updatedTemporary!); } if (selection != null) { - currentIndexCubit.changeSelection(selection); + editorController.toolCubit.changeSelection(selection); } }); on((event, emit) { @@ -799,19 +817,17 @@ class DocumentBloc extends ReplayBloc { } final item = tools.removeAt(oldIndex); tools.insert(newIndex, item); - final cubit = currentIndexCubit; - var nextCurrentIndex = cubit.state.index; - if (nextCurrentIndex != null) { - if (nextCurrentIndex == oldIndex) { - nextCurrentIndex = newIndex; - } else if (nextCurrentIndex > oldIndex && - nextCurrentIndex <= newIndex) { - nextCurrentIndex -= 1; - } else if (nextCurrentIndex < oldIndex && - nextCurrentIndex >= newIndex) { - nextCurrentIndex += 1; + final cubit = editorController; + var nextToolIndex = cubit.toolCubit.state.index; + if (nextToolIndex != null) { + if (nextToolIndex == oldIndex) { + nextToolIndex = newIndex; + } else if (nextToolIndex > oldIndex && nextToolIndex <= newIndex) { + nextToolIndex -= 1; + } else if (nextToolIndex < oldIndex && nextToolIndex >= newIndex) { + nextToolIndex += 1; } - cubit.changeIndex(nextCurrentIndex); + cubit.toolCubit.setIndex(nextToolIndex); } _saveState( emit, @@ -836,7 +852,8 @@ class DocumentBloc extends ReplayBloc { } }); final data = current.data.removeAssets(unusedAssets.toList()); - for (final bg in currentIndexCubit.state.cameraViewport.backgrounds) { + for (final bg + in editorController.rendererCubit.state.cameraViewport.backgrounds) { bg.dispose(); } _saveState( @@ -993,7 +1010,10 @@ class DocumentBloc extends ReplayBloc { } final newState = current.copyWith(invisibleLayers: invisibleLayers); emit(newState); - currentIndexCubit.unbake(newState); + editorController.editorSessionCubit?.updateLayer( + invisibleLayers: invisibleLayers, + ); + editorController.unbake(newState); }); on((event, emit) async { @@ -1134,6 +1154,9 @@ class DocumentBloc extends ReplayBloc { if (current is! DocumentLoadSuccess) return; if (!(embedding?.editable ?? true)) return; emit(current.copyWith(currentLayer: event.name)); + editorController.editorSessionCubit?.updateLayer( + currentLayer: event.name, + ); }); on((event, emit) { @@ -1141,6 +1164,9 @@ class DocumentBloc extends ReplayBloc { if (current is! DocumentLoadSuccess) return; if (!(embedding?.editable ?? true)) return; emit(current.copyWith(currentCollection: event.name)); + editorController.editorSessionCubit?.updateLayer( + currentCollection: event.name, + ); }); on((event, emit) { @@ -1198,7 +1224,7 @@ class DocumentBloc extends ReplayBloc { .toList(); if (areas.isEmpty) continue; if (pageName == current.pageName) { - for (var element in currentIndexCubit.renderers) { + for (var element in editorController.rendererCubit.renderers) { final needRepaint = areas.any( (area) => element.onAreaUpdate(data, page, area), ); @@ -1293,7 +1319,7 @@ class DocumentBloc extends ReplayBloc { currentPage = updatedPage; currentPageChanged = true; if (entry.value.contains(currentAreaName)) currentAreaName = ''; - for (var element in currentIndexCubit.renderers) { + for (var element in editorController.rendererCubit.renderers) { if (areas.contains(element.area) && element.onAreaUpdate(current.data, currentPage, null)) { shouldRepaint = true; @@ -1316,7 +1342,7 @@ class DocumentBloc extends ReplayBloc { final current = state; if (current is! DocumentLoadSuccess) return; if (!(embedding?.editable ?? true)) return; - final oldSelection = currentIndexCubit.state.selection; + final oldSelection = editorController.toolCubit.state.selection; var selection = oldSelection; final hasInitial = event.area.isInitial; Area? previousArea; @@ -1343,7 +1369,7 @@ class DocumentBloc extends ReplayBloc { event.area.position.y - oldArea.position.y, ); if (delta != Offset.zero) { - for (final renderer in currentIndexCubit.renderers) { + for (final renderer in editorController.rendererCubit.renderers) { final id = renderer.element.id; final rect = renderer.expandedRect ?? renderer.rect; if (id == null || rect == null || !oldArea.rect.overlaps(rect)) { @@ -1365,13 +1391,13 @@ class DocumentBloc extends ReplayBloc { } } } - final shouldRepaint = currentIndexCubit.renderers.any( + final shouldRepaint = editorController.rendererCubit.renderers.any( (element) => element.area?.name == event.name && element.onAreaUpdate(current.data, currentDocument, event.area), ); if (selection != null && selection != oldSelection) { - currentIndexCubit.changeSelection(selection); + editorController.toolCubit.changeSelection(selection); } _saveState( emit, @@ -1525,14 +1551,14 @@ class DocumentBloc extends ReplayBloc { windowCubit: current.windowCubit, absolute: current.absolute, ); - currentIndexCubit.updateHandler(this, newState.handler); + editorController.updateHandler(this, newState.handler); emit(newState); }); on((event, emit) { final current = state; if (current is! DocumentPresentationState) return; emit(current.oldState); - currentIndexCubit.changeTool(this); + editorController.changeTool(this); setFullScreen(current.fullScreen); }); on((event, emit) { @@ -1547,7 +1573,7 @@ class DocumentBloc extends ReplayBloc { if (!validAssetPaths.any((e) => event.path.startsWith('$e/'))) return; data = data.setAsset(event.path, Uint8List.fromList(event.data)); current.assetService.invalidate(event.path); - final updatedRenderers = currentIndexCubit.renderers + final updatedRenderers = editorController.rendererCubit.renderers .where( (e) => e.onAssetUpdate( data, @@ -1557,7 +1583,7 @@ class DocumentBloc extends ReplayBloc { ), ) .toList(); - currentIndexCubit.invalidateRenderers(updatedRenderers); + editorController.invalidateRenderers(updatedRenderers); _saveState( emit, state: current.copyWith(data: data), @@ -1662,7 +1688,7 @@ class DocumentBloc extends ReplayBloc { : null; state ??= this.state as DocumentLoadSuccess; emit(state); - return currentIndexCubit.stateChanged( + return editorController.stateChanged( state, this, oldState: oldState, @@ -1687,7 +1713,7 @@ class DocumentBloc extends ReplayBloc { Future refresh({bool allowBake = true}) async { final current = state; - final cubit = _currentIndexCubit; + final cubit = _editorController; if (current is! DocumentLoadSuccess || cubit == null) return; return cubit.refresh(current, allowBake: allowBake); } @@ -1696,18 +1722,18 @@ class DocumentBloc extends ReplayBloc { /// Use this when handler internal state changes but document hasn't changed. Future refreshForegrounds() async { final current = state; - final cubit = _currentIndexCubit; + final cubit = _editorController; if (current is! DocumentLoadSuccess || cubit == null) return; return cubit.refreshForegrounds(current); } /// Ultra-lightweight update for cursor changes only. void updateCursor(MouseCursor cursor) { - _currentIndexCubit?.updateCursor(cursor); + _editorController?.updateCursor(cursor); } Future refreshToolbar() => - _currentIndexCubit?.refreshToolbar(this) ?? Future.value(); + _editorController?.refreshToolbar(this) ?? Future.value(); Future bake({ Size? viewportSize, @@ -1715,7 +1741,7 @@ class DocumentBloc extends ReplayBloc { bool reset = false, }) async { final current = state; - final cubit = _currentIndexCubit; + final cubit = _editorController; if (current is! DocumentLoaded || cubit == null) return; return cubit.bake( current, @@ -1732,7 +1758,7 @@ class DocumentBloc extends ReplayBloc { bool testTransform = false, }) { final current = state; - final cubit = _currentIndexCubit; + final cubit = _editorController; if (current is! DocumentLoaded || cubit == null) return Future.value(); return cubit.delayedBake( current, @@ -1744,28 +1770,28 @@ class DocumentBloc extends ReplayBloc { } void cancelDelayedBake() { - _currentIndexCubit?.cancelDelayedBake(); + _editorController?.cancelDelayedBake(); } Future load() async { final current = state; - final cubit = _currentIndexCubit; + final cubit = _editorController; if (current is! DocumentLoaded || cubit == null) return; - if (!cubit.state.location.isEmpty) { - cubit.setSaveState( + if (!cubit.saveCubit.state.location.isEmpty) { + cubit.saveCubit.setSaveState( saved: SaveState.saved, isCreating: false, keepRead: true, ); } else { - cubit.setSaveState(isCreating: true); + cubit.saveCubit.setSaveState(isCreating: true); } await cubit.loadElements(current); cubit.init(this); } Future reload() async { - return _currentIndexCubit?.reload(this); + return _editorController?.reload(this); } void _scheduleHistoryReload() => @@ -1777,9 +1803,9 @@ class DocumentBloc extends ReplayBloc { String? name, }) async { final current = state; - final cubit = _currentIndexCubit; + final cubit = _editorController; if (current is! DocumentLoadSuccess || cubit == null) return; - final data = await current.saveData(null, cubit.state.viewOption); + final data = await current.saveData(); final render = await cubit.render( current.data, current.page, @@ -1811,9 +1837,9 @@ class DocumentBloc extends ReplayBloc { FileMetadata metadata, ) async { final current = state; - final cubit = _currentIndexCubit; + final cubit = _editorController; if (current is! DocumentLoadSuccess || cubit == null) return; - final data = await current.saveData(null, cubit.state.viewOption); + final data = await current.saveData(); await templateSystem.updateFile( path, data.createTemplate( @@ -1829,17 +1855,17 @@ class DocumentBloc extends ReplayBloc { await _historyReloadRunner.disposeAndWait(); clearHistory(); final currentState = state; - final currentIndexCubit = _currentIndexCubit; - final transformCubit = currentIndexCubit?.transformCubit; + final editorController = _editorController; + final transformCubit = editorController?.transformCubit; final assetService = _assetService; - if (currentIndexCubit != null && !currentIndexCubit.isClosed) { - await currentIndexCubit.close(); + if (editorController != null && !editorController.isClosed) { + await editorController.close(); } if (transformCubit != null && !transformCubit.isClosed) { await transformCubit.close(); } await assetService?.dispose(); - _currentIndexCubit = null; + _editorController = null; _assetService = null; if (currentState is DocumentLoaded && !isClosed) { emit( @@ -1856,7 +1882,7 @@ class DocumentBloc extends ReplayBloc { AssetLocation? location, bool force = false, bool isAutosave = false, - }) async => await _currentIndexCubit?.save( + }) async => await _editorController?.save( this, location: location, force: force, @@ -1896,10 +1922,10 @@ class DocumentBloc extends ReplayBloc { HitElementMode? hitElementMode, }) async { final state = this.state; - final cubit = _currentIndexCubit; + final cubit = _editorController; if (state is! DocumentLoadSuccess || cubit == null) return {}; transform ??= cubit.transformCubit.state; - final renderers = cubit.state.cameraViewport.visibleElements; + final renderers = cubit.rendererCubit.state.cameraViewport.visibleElements; if (renderers.isEmpty) return {}; hitElementMode ??= HitElementMode.touchAnywhere; @@ -1931,9 +1957,9 @@ class DocumentBloc extends ReplayBloc { HitElementMode? hitElementMode, }) async { final state = this.state; - final cubit = _currentIndexCubit; + final cubit = _editorController; if (state is! DocumentLoadSuccess || cubit == null) return {}; - final renderers = cubit.state.cameraViewport.visibleElements; + final renderers = cubit.rendererCubit.state.cameraViewport.visibleElements; if (renderers.isEmpty) return {}; transform ??= cubit.transformCubit.state; hitElementMode ??= HitElementMode.touchAnywhere; diff --git a/app/lib/bloc/document_state.dart b/app/lib/bloc/document_state.dart index 110a5eb3fae6..3dcbd08f9907 100644 --- a/app/lib/bloc/document_state.dart +++ b/app/lib/bloc/document_state.dart @@ -94,7 +94,7 @@ abstract class DocumentLoaded extends DocumentState { @override Future saveData([NoteData? current, ViewOption? viewOption]) async { current ??= data; - viewOption ??= const ViewOption(); + viewOption ??= info.view; current = await _updatePage(current); current = _updateMetadata(current); current = _updateInfo(current, viewOption); diff --git a/app/lib/cubits/current_index.freezed.dart b/app/lib/cubits/current_index.freezed.dart deleted file mode 100644 index 9251b318d286..000000000000 --- a/app/lib/cubits/current_index.freezed.dart +++ /dev/null @@ -1,357 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// coverage:ignore-file -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'current_index.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; -/// @nodoc -mixin _$CurrentIndex implements DiagnosticableTreeMixin { - - int? get index; Handler get handler; CameraViewport get cameraViewport; bool get isSaveDelayed; UtilitiesState get utilities; Handler? get temporaryHandler; int? get temporaryIndex; List get foregrounds; Selection? get selection; bool get pinned; List? get temporaryForegrounds; Map> get toggleableHandlers; List get networkingForegrounds; Map> get toggleableForegrounds; MouseCursor get cursor; MouseCursor? get temporaryCursor; TemporaryState get temporaryState; Offset? get lastPosition; List get pointers; int? get buttons; AssetLocation get location; Embedding? get embedding; SaveState get saved; PreferredSizeWidget? get toolbar; PreferredSizeWidget? get temporaryToolbar; Map get rendererStates; Map? get temporaryRendererStates; ViewOption get viewOption; HideState get hideUi; bool get areaNavigatorCreate; bool get areaNavigatorExact; bool get areaNavigatorAsk; bool get navigatorEnabled; NavigatorPage get navigatorPage; bool get isCreating; String get userName; bool get penDetected; bool get sessionPenOnlyInput; -/// Create a copy of CurrentIndex -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$CurrentIndexCopyWith get copyWith => _$CurrentIndexCopyWithImpl(this as CurrentIndex, _$identity); - - -@override -void debugFillProperties(DiagnosticPropertiesBuilder properties) { - properties - ..add(DiagnosticsProperty('type', 'CurrentIndex')) - ..add(DiagnosticsProperty('index', index))..add(DiagnosticsProperty('handler', handler))..add(DiagnosticsProperty('cameraViewport', cameraViewport))..add(DiagnosticsProperty('isSaveDelayed', isSaveDelayed))..add(DiagnosticsProperty('utilities', utilities))..add(DiagnosticsProperty('temporaryHandler', temporaryHandler))..add(DiagnosticsProperty('temporaryIndex', temporaryIndex))..add(DiagnosticsProperty('foregrounds', foregrounds))..add(DiagnosticsProperty('selection', selection))..add(DiagnosticsProperty('pinned', pinned))..add(DiagnosticsProperty('temporaryForegrounds', temporaryForegrounds))..add(DiagnosticsProperty('toggleableHandlers', toggleableHandlers))..add(DiagnosticsProperty('networkingForegrounds', networkingForegrounds))..add(DiagnosticsProperty('toggleableForegrounds', toggleableForegrounds))..add(DiagnosticsProperty('cursor', cursor))..add(DiagnosticsProperty('temporaryCursor', temporaryCursor))..add(DiagnosticsProperty('temporaryState', temporaryState))..add(DiagnosticsProperty('lastPosition', lastPosition))..add(DiagnosticsProperty('pointers', pointers))..add(DiagnosticsProperty('buttons', buttons))..add(DiagnosticsProperty('location', location))..add(DiagnosticsProperty('embedding', embedding))..add(DiagnosticsProperty('saved', saved))..add(DiagnosticsProperty('toolbar', toolbar))..add(DiagnosticsProperty('temporaryToolbar', temporaryToolbar))..add(DiagnosticsProperty('rendererStates', rendererStates))..add(DiagnosticsProperty('temporaryRendererStates', temporaryRendererStates))..add(DiagnosticsProperty('viewOption', viewOption))..add(DiagnosticsProperty('hideUi', hideUi))..add(DiagnosticsProperty('areaNavigatorCreate', areaNavigatorCreate))..add(DiagnosticsProperty('areaNavigatorExact', areaNavigatorExact))..add(DiagnosticsProperty('areaNavigatorAsk', areaNavigatorAsk))..add(DiagnosticsProperty('navigatorEnabled', navigatorEnabled))..add(DiagnosticsProperty('navigatorPage', navigatorPage))..add(DiagnosticsProperty('isCreating', isCreating))..add(DiagnosticsProperty('userName', userName))..add(DiagnosticsProperty('penDetected', penDetected))..add(DiagnosticsProperty('sessionPenOnlyInput', sessionPenOnlyInput)); -} - - - -@override -String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'CurrentIndex(index: $index, handler: $handler, cameraViewport: $cameraViewport, isSaveDelayed: $isSaveDelayed, utilities: $utilities, temporaryHandler: $temporaryHandler, temporaryIndex: $temporaryIndex, foregrounds: $foregrounds, selection: $selection, pinned: $pinned, temporaryForegrounds: $temporaryForegrounds, toggleableHandlers: $toggleableHandlers, networkingForegrounds: $networkingForegrounds, toggleableForegrounds: $toggleableForegrounds, cursor: $cursor, temporaryCursor: $temporaryCursor, temporaryState: $temporaryState, lastPosition: $lastPosition, pointers: $pointers, buttons: $buttons, location: $location, embedding: $embedding, saved: $saved, toolbar: $toolbar, temporaryToolbar: $temporaryToolbar, rendererStates: $rendererStates, temporaryRendererStates: $temporaryRendererStates, viewOption: $viewOption, hideUi: $hideUi, areaNavigatorCreate: $areaNavigatorCreate, areaNavigatorExact: $areaNavigatorExact, areaNavigatorAsk: $areaNavigatorAsk, navigatorEnabled: $navigatorEnabled, navigatorPage: $navigatorPage, isCreating: $isCreating, userName: $userName, penDetected: $penDetected, sessionPenOnlyInput: $sessionPenOnlyInput)'; -} - - -} - -/// @nodoc -abstract mixin class $CurrentIndexCopyWith<$Res> { - factory $CurrentIndexCopyWith(CurrentIndex value, $Res Function(CurrentIndex) _then) = _$CurrentIndexCopyWithImpl; -@useResult -$Res call({ - int? index, Handler handler, CameraViewport cameraViewport, bool isSaveDelayed, UtilitiesState utilities, Handler? temporaryHandler, int? temporaryIndex, List foregrounds, Selection? selection, bool pinned, List? temporaryForegrounds, Map> toggleableHandlers, List networkingForegrounds, Map> toggleableForegrounds, MouseCursor cursor, MouseCursor? temporaryCursor, TemporaryState temporaryState, Offset? lastPosition, List pointers, int? buttons, AssetLocation location, Embedding? embedding, SaveState saved, PreferredSizeWidget? toolbar, PreferredSizeWidget? temporaryToolbar, Map rendererStates, Map? temporaryRendererStates, ViewOption viewOption, HideState hideUi, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, bool navigatorEnabled, NavigatorPage navigatorPage, bool isCreating, String userName, bool penDetected, bool sessionPenOnlyInput -}); - - -$CameraViewportCopyWith<$Res> get cameraViewport;$UtilitiesStateCopyWith<$Res> get utilities;$ViewOptionCopyWith<$Res> get viewOption; - -} -/// @nodoc -class _$CurrentIndexCopyWithImpl<$Res> - implements $CurrentIndexCopyWith<$Res> { - _$CurrentIndexCopyWithImpl(this._self, this._then); - - final CurrentIndex _self; - final $Res Function(CurrentIndex) _then; - -/// Create a copy of CurrentIndex -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? index = freezed,Object? handler = null,Object? cameraViewport = null,Object? isSaveDelayed = null,Object? utilities = null,Object? temporaryHandler = freezed,Object? temporaryIndex = freezed,Object? foregrounds = null,Object? selection = freezed,Object? pinned = null,Object? temporaryForegrounds = freezed,Object? toggleableHandlers = null,Object? networkingForegrounds = null,Object? toggleableForegrounds = null,Object? cursor = null,Object? temporaryCursor = freezed,Object? temporaryState = null,Object? lastPosition = freezed,Object? pointers = null,Object? buttons = freezed,Object? location = null,Object? embedding = freezed,Object? saved = null,Object? toolbar = freezed,Object? temporaryToolbar = freezed,Object? rendererStates = null,Object? temporaryRendererStates = freezed,Object? viewOption = null,Object? hideUi = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? isCreating = null,Object? userName = null,Object? penDetected = null,Object? sessionPenOnlyInput = null,}) { - return _then(_self.copyWith( -index: freezed == index ? _self.index : index // ignore: cast_nullable_to_non_nullable -as int?,handler: null == handler ? _self.handler : handler // ignore: cast_nullable_to_non_nullable -as Handler,cameraViewport: null == cameraViewport ? _self.cameraViewport : cameraViewport // ignore: cast_nullable_to_non_nullable -as CameraViewport,isSaveDelayed: null == isSaveDelayed ? _self.isSaveDelayed : isSaveDelayed // ignore: cast_nullable_to_non_nullable -as bool,utilities: null == utilities ? _self.utilities : utilities // ignore: cast_nullable_to_non_nullable -as UtilitiesState,temporaryHandler: freezed == temporaryHandler ? _self.temporaryHandler : temporaryHandler // ignore: cast_nullable_to_non_nullable -as Handler?,temporaryIndex: freezed == temporaryIndex ? _self.temporaryIndex : temporaryIndex // ignore: cast_nullable_to_non_nullable -as int?,foregrounds: null == foregrounds ? _self.foregrounds : foregrounds // ignore: cast_nullable_to_non_nullable -as List,selection: freezed == selection ? _self.selection : selection // ignore: cast_nullable_to_non_nullable -as Selection?,pinned: null == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable -as bool,temporaryForegrounds: freezed == temporaryForegrounds ? _self.temporaryForegrounds : temporaryForegrounds // ignore: cast_nullable_to_non_nullable -as List?,toggleableHandlers: null == toggleableHandlers ? _self.toggleableHandlers : toggleableHandlers // ignore: cast_nullable_to_non_nullable -as Map>,networkingForegrounds: null == networkingForegrounds ? _self.networkingForegrounds : networkingForegrounds // ignore: cast_nullable_to_non_nullable -as List,toggleableForegrounds: null == toggleableForegrounds ? _self.toggleableForegrounds : toggleableForegrounds // ignore: cast_nullable_to_non_nullable -as Map>,cursor: null == cursor ? _self.cursor : cursor // ignore: cast_nullable_to_non_nullable -as MouseCursor,temporaryCursor: freezed == temporaryCursor ? _self.temporaryCursor : temporaryCursor // ignore: cast_nullable_to_non_nullable -as MouseCursor?,temporaryState: null == temporaryState ? _self.temporaryState : temporaryState // ignore: cast_nullable_to_non_nullable -as TemporaryState,lastPosition: freezed == lastPosition ? _self.lastPosition : lastPosition // ignore: cast_nullable_to_non_nullable -as Offset?,pointers: null == pointers ? _self.pointers : pointers // ignore: cast_nullable_to_non_nullable -as List,buttons: freezed == buttons ? _self.buttons : buttons // ignore: cast_nullable_to_non_nullable -as int?,location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable -as AssetLocation,embedding: freezed == embedding ? _self.embedding : embedding // ignore: cast_nullable_to_non_nullable -as Embedding?,saved: null == saved ? _self.saved : saved // ignore: cast_nullable_to_non_nullable -as SaveState,toolbar: freezed == toolbar ? _self.toolbar : toolbar // ignore: cast_nullable_to_non_nullable -as PreferredSizeWidget?,temporaryToolbar: freezed == temporaryToolbar ? _self.temporaryToolbar : temporaryToolbar // ignore: cast_nullable_to_non_nullable -as PreferredSizeWidget?,rendererStates: null == rendererStates ? _self.rendererStates : rendererStates // ignore: cast_nullable_to_non_nullable -as Map,temporaryRendererStates: freezed == temporaryRendererStates ? _self.temporaryRendererStates : temporaryRendererStates // ignore: cast_nullable_to_non_nullable -as Map?,viewOption: null == viewOption ? _self.viewOption : viewOption // ignore: cast_nullable_to_non_nullable -as ViewOption,hideUi: null == hideUi ? _self.hideUi : hideUi // ignore: cast_nullable_to_non_nullable -as HideState,areaNavigatorCreate: null == areaNavigatorCreate ? _self.areaNavigatorCreate : areaNavigatorCreate // ignore: cast_nullable_to_non_nullable -as bool,areaNavigatorExact: null == areaNavigatorExact ? _self.areaNavigatorExact : areaNavigatorExact // ignore: cast_nullable_to_non_nullable -as bool,areaNavigatorAsk: null == areaNavigatorAsk ? _self.areaNavigatorAsk : areaNavigatorAsk // ignore: cast_nullable_to_non_nullable -as bool,navigatorEnabled: null == navigatorEnabled ? _self.navigatorEnabled : navigatorEnabled // ignore: cast_nullable_to_non_nullable -as bool,navigatorPage: null == navigatorPage ? _self.navigatorPage : navigatorPage // ignore: cast_nullable_to_non_nullable -as NavigatorPage,isCreating: null == isCreating ? _self.isCreating : isCreating // ignore: cast_nullable_to_non_nullable -as bool,userName: null == userName ? _self.userName : userName // ignore: cast_nullable_to_non_nullable -as String,penDetected: null == penDetected ? _self.penDetected : penDetected // ignore: cast_nullable_to_non_nullable -as bool,sessionPenOnlyInput: null == sessionPenOnlyInput ? _self.sessionPenOnlyInput : sessionPenOnlyInput // ignore: cast_nullable_to_non_nullable -as bool, - )); -} -/// Create a copy of CurrentIndex -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$CameraViewportCopyWith<$Res> get cameraViewport { - - return $CameraViewportCopyWith<$Res>(_self.cameraViewport, (value) { - return _then(_self.copyWith(cameraViewport: value)); - }); -}/// Create a copy of CurrentIndex -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$UtilitiesStateCopyWith<$Res> get utilities { - - return $UtilitiesStateCopyWith<$Res>(_self.utilities, (value) { - return _then(_self.copyWith(utilities: value)); - }); -}/// Create a copy of CurrentIndex -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$ViewOptionCopyWith<$Res> get viewOption { - - return $ViewOptionCopyWith<$Res>(_self.viewOption, (value) { - return _then(_self.copyWith(viewOption: value)); - }); -} -} - - - -/// @nodoc - - -class _CurrentIndex extends CurrentIndex with DiagnosticableTreeMixin { - const _CurrentIndex(this.index, this.handler, this.cameraViewport, {this.isSaveDelayed = false, this.utilities = const UtilitiesState(), this.temporaryHandler, this.temporaryIndex, final List foregrounds = const [], this.selection, this.pinned = false, final List? temporaryForegrounds, final Map> toggleableHandlers = const {}, final List networkingForegrounds = const [], final Map> toggleableForegrounds = const {}, this.cursor = MouseCursor.defer, this.temporaryCursor, this.temporaryState = TemporaryState.allowClick, this.lastPosition, final List pointers = const [], this.buttons, this.location = const AssetLocation(path: ''), this.embedding, this.saved = SaveState.saved, this.toolbar, this.temporaryToolbar, final Map rendererStates = const {}, final Map? temporaryRendererStates = const {}, this.viewOption = const ViewOption(), this.hideUi = HideState.visible, this.areaNavigatorCreate = true, this.areaNavigatorExact = true, this.areaNavigatorAsk = false, this.navigatorEnabled = false, this.navigatorPage = NavigatorPage.waypoints, this.isCreating = false, this.userName = '', this.penDetected = false, this.sessionPenOnlyInput = false}): _foregrounds = foregrounds,_temporaryForegrounds = temporaryForegrounds,_toggleableHandlers = toggleableHandlers,_networkingForegrounds = networkingForegrounds,_toggleableForegrounds = toggleableForegrounds,_pointers = pointers,_rendererStates = rendererStates,_temporaryRendererStates = temporaryRendererStates,super._(); - - -@override final int? index; -@override final Handler handler; -@override final CameraViewport cameraViewport; -@override@JsonKey() final bool isSaveDelayed; -@override@JsonKey() final UtilitiesState utilities; -@override final Handler? temporaryHandler; -@override final int? temporaryIndex; - final List _foregrounds; -@override@JsonKey() List get foregrounds { - if (_foregrounds is EqualUnmodifiableListView) return _foregrounds; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_foregrounds); -} - -@override final Selection? selection; -@override@JsonKey() final bool pinned; - final List? _temporaryForegrounds; -@override List? get temporaryForegrounds { - final value = _temporaryForegrounds; - if (value == null) return null; - if (_temporaryForegrounds is EqualUnmodifiableListView) return _temporaryForegrounds; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(value); -} - - final Map> _toggleableHandlers; -@override@JsonKey() Map> get toggleableHandlers { - if (_toggleableHandlers is EqualUnmodifiableMapView) return _toggleableHandlers; - // ignore: implicit_dynamic_type - return EqualUnmodifiableMapView(_toggleableHandlers); -} - - final List _networkingForegrounds; -@override@JsonKey() List get networkingForegrounds { - if (_networkingForegrounds is EqualUnmodifiableListView) return _networkingForegrounds; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_networkingForegrounds); -} - - final Map> _toggleableForegrounds; -@override@JsonKey() Map> get toggleableForegrounds { - if (_toggleableForegrounds is EqualUnmodifiableMapView) return _toggleableForegrounds; - // ignore: implicit_dynamic_type - return EqualUnmodifiableMapView(_toggleableForegrounds); -} - -@override@JsonKey() final MouseCursor cursor; -@override final MouseCursor? temporaryCursor; -@override@JsonKey() final TemporaryState temporaryState; -@override final Offset? lastPosition; - final List _pointers; -@override@JsonKey() List get pointers { - if (_pointers is EqualUnmodifiableListView) return _pointers; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_pointers); -} - -@override final int? buttons; -@override@JsonKey() final AssetLocation location; -@override final Embedding? embedding; -@override@JsonKey() final SaveState saved; -@override final PreferredSizeWidget? toolbar; -@override final PreferredSizeWidget? temporaryToolbar; - final Map _rendererStates; -@override@JsonKey() Map get rendererStates { - if (_rendererStates is EqualUnmodifiableMapView) return _rendererStates; - // ignore: implicit_dynamic_type - return EqualUnmodifiableMapView(_rendererStates); -} - - final Map? _temporaryRendererStates; -@override@JsonKey() Map? get temporaryRendererStates { - final value = _temporaryRendererStates; - if (value == null) return null; - if (_temporaryRendererStates is EqualUnmodifiableMapView) return _temporaryRendererStates; - // ignore: implicit_dynamic_type - return EqualUnmodifiableMapView(value); -} - -@override@JsonKey() final ViewOption viewOption; -@override@JsonKey() final HideState hideUi; -@override@JsonKey() final bool areaNavigatorCreate; -@override@JsonKey() final bool areaNavigatorExact; -@override@JsonKey() final bool areaNavigatorAsk; -@override@JsonKey() final bool navigatorEnabled; -@override@JsonKey() final NavigatorPage navigatorPage; -@override@JsonKey() final bool isCreating; -@override@JsonKey() final String userName; -@override@JsonKey() final bool penDetected; -@override@JsonKey() final bool sessionPenOnlyInput; - -/// Create a copy of CurrentIndex -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$CurrentIndexCopyWith<_CurrentIndex> get copyWith => __$CurrentIndexCopyWithImpl<_CurrentIndex>(this, _$identity); - - -@override -void debugFillProperties(DiagnosticPropertiesBuilder properties) { - properties - ..add(DiagnosticsProperty('type', 'CurrentIndex')) - ..add(DiagnosticsProperty('index', index))..add(DiagnosticsProperty('handler', handler))..add(DiagnosticsProperty('cameraViewport', cameraViewport))..add(DiagnosticsProperty('isSaveDelayed', isSaveDelayed))..add(DiagnosticsProperty('utilities', utilities))..add(DiagnosticsProperty('temporaryHandler', temporaryHandler))..add(DiagnosticsProperty('temporaryIndex', temporaryIndex))..add(DiagnosticsProperty('foregrounds', foregrounds))..add(DiagnosticsProperty('selection', selection))..add(DiagnosticsProperty('pinned', pinned))..add(DiagnosticsProperty('temporaryForegrounds', temporaryForegrounds))..add(DiagnosticsProperty('toggleableHandlers', toggleableHandlers))..add(DiagnosticsProperty('networkingForegrounds', networkingForegrounds))..add(DiagnosticsProperty('toggleableForegrounds', toggleableForegrounds))..add(DiagnosticsProperty('cursor', cursor))..add(DiagnosticsProperty('temporaryCursor', temporaryCursor))..add(DiagnosticsProperty('temporaryState', temporaryState))..add(DiagnosticsProperty('lastPosition', lastPosition))..add(DiagnosticsProperty('pointers', pointers))..add(DiagnosticsProperty('buttons', buttons))..add(DiagnosticsProperty('location', location))..add(DiagnosticsProperty('embedding', embedding))..add(DiagnosticsProperty('saved', saved))..add(DiagnosticsProperty('toolbar', toolbar))..add(DiagnosticsProperty('temporaryToolbar', temporaryToolbar))..add(DiagnosticsProperty('rendererStates', rendererStates))..add(DiagnosticsProperty('temporaryRendererStates', temporaryRendererStates))..add(DiagnosticsProperty('viewOption', viewOption))..add(DiagnosticsProperty('hideUi', hideUi))..add(DiagnosticsProperty('areaNavigatorCreate', areaNavigatorCreate))..add(DiagnosticsProperty('areaNavigatorExact', areaNavigatorExact))..add(DiagnosticsProperty('areaNavigatorAsk', areaNavigatorAsk))..add(DiagnosticsProperty('navigatorEnabled', navigatorEnabled))..add(DiagnosticsProperty('navigatorPage', navigatorPage))..add(DiagnosticsProperty('isCreating', isCreating))..add(DiagnosticsProperty('userName', userName))..add(DiagnosticsProperty('penDetected', penDetected))..add(DiagnosticsProperty('sessionPenOnlyInput', sessionPenOnlyInput)); -} - - - -@override -String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'CurrentIndex(index: $index, handler: $handler, cameraViewport: $cameraViewport, isSaveDelayed: $isSaveDelayed, utilities: $utilities, temporaryHandler: $temporaryHandler, temporaryIndex: $temporaryIndex, foregrounds: $foregrounds, selection: $selection, pinned: $pinned, temporaryForegrounds: $temporaryForegrounds, toggleableHandlers: $toggleableHandlers, networkingForegrounds: $networkingForegrounds, toggleableForegrounds: $toggleableForegrounds, cursor: $cursor, temporaryCursor: $temporaryCursor, temporaryState: $temporaryState, lastPosition: $lastPosition, pointers: $pointers, buttons: $buttons, location: $location, embedding: $embedding, saved: $saved, toolbar: $toolbar, temporaryToolbar: $temporaryToolbar, rendererStates: $rendererStates, temporaryRendererStates: $temporaryRendererStates, viewOption: $viewOption, hideUi: $hideUi, areaNavigatorCreate: $areaNavigatorCreate, areaNavigatorExact: $areaNavigatorExact, areaNavigatorAsk: $areaNavigatorAsk, navigatorEnabled: $navigatorEnabled, navigatorPage: $navigatorPage, isCreating: $isCreating, userName: $userName, penDetected: $penDetected, sessionPenOnlyInput: $sessionPenOnlyInput)'; -} - - -} - -/// @nodoc -abstract mixin class _$CurrentIndexCopyWith<$Res> implements $CurrentIndexCopyWith<$Res> { - factory _$CurrentIndexCopyWith(_CurrentIndex value, $Res Function(_CurrentIndex) _then) = __$CurrentIndexCopyWithImpl; -@override @useResult -$Res call({ - int? index, Handler handler, CameraViewport cameraViewport, bool isSaveDelayed, UtilitiesState utilities, Handler? temporaryHandler, int? temporaryIndex, List foregrounds, Selection? selection, bool pinned, List? temporaryForegrounds, Map> toggleableHandlers, List networkingForegrounds, Map> toggleableForegrounds, MouseCursor cursor, MouseCursor? temporaryCursor, TemporaryState temporaryState, Offset? lastPosition, List pointers, int? buttons, AssetLocation location, Embedding? embedding, SaveState saved, PreferredSizeWidget? toolbar, PreferredSizeWidget? temporaryToolbar, Map rendererStates, Map? temporaryRendererStates, ViewOption viewOption, HideState hideUi, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, bool navigatorEnabled, NavigatorPage navigatorPage, bool isCreating, String userName, bool penDetected, bool sessionPenOnlyInput -}); - - -@override $CameraViewportCopyWith<$Res> get cameraViewport;@override $UtilitiesStateCopyWith<$Res> get utilities;@override $ViewOptionCopyWith<$Res> get viewOption; - -} -/// @nodoc -class __$CurrentIndexCopyWithImpl<$Res> - implements _$CurrentIndexCopyWith<$Res> { - __$CurrentIndexCopyWithImpl(this._self, this._then); - - final _CurrentIndex _self; - final $Res Function(_CurrentIndex) _then; - -/// Create a copy of CurrentIndex -/// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? index = freezed,Object? handler = null,Object? cameraViewport = null,Object? isSaveDelayed = null,Object? utilities = null,Object? temporaryHandler = freezed,Object? temporaryIndex = freezed,Object? foregrounds = null,Object? selection = freezed,Object? pinned = null,Object? temporaryForegrounds = freezed,Object? toggleableHandlers = null,Object? networkingForegrounds = null,Object? toggleableForegrounds = null,Object? cursor = null,Object? temporaryCursor = freezed,Object? temporaryState = null,Object? lastPosition = freezed,Object? pointers = null,Object? buttons = freezed,Object? location = null,Object? embedding = freezed,Object? saved = null,Object? toolbar = freezed,Object? temporaryToolbar = freezed,Object? rendererStates = null,Object? temporaryRendererStates = freezed,Object? viewOption = null,Object? hideUi = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? isCreating = null,Object? userName = null,Object? penDetected = null,Object? sessionPenOnlyInput = null,}) { - return _then(_CurrentIndex( -freezed == index ? _self.index : index // ignore: cast_nullable_to_non_nullable -as int?,null == handler ? _self.handler : handler // ignore: cast_nullable_to_non_nullable -as Handler,null == cameraViewport ? _self.cameraViewport : cameraViewport // ignore: cast_nullable_to_non_nullable -as CameraViewport,isSaveDelayed: null == isSaveDelayed ? _self.isSaveDelayed : isSaveDelayed // ignore: cast_nullable_to_non_nullable -as bool,utilities: null == utilities ? _self.utilities : utilities // ignore: cast_nullable_to_non_nullable -as UtilitiesState,temporaryHandler: freezed == temporaryHandler ? _self.temporaryHandler : temporaryHandler // ignore: cast_nullable_to_non_nullable -as Handler?,temporaryIndex: freezed == temporaryIndex ? _self.temporaryIndex : temporaryIndex // ignore: cast_nullable_to_non_nullable -as int?,foregrounds: null == foregrounds ? _self._foregrounds : foregrounds // ignore: cast_nullable_to_non_nullable -as List,selection: freezed == selection ? _self.selection : selection // ignore: cast_nullable_to_non_nullable -as Selection?,pinned: null == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable -as bool,temporaryForegrounds: freezed == temporaryForegrounds ? _self._temporaryForegrounds : temporaryForegrounds // ignore: cast_nullable_to_non_nullable -as List?,toggleableHandlers: null == toggleableHandlers ? _self._toggleableHandlers : toggleableHandlers // ignore: cast_nullable_to_non_nullable -as Map>,networkingForegrounds: null == networkingForegrounds ? _self._networkingForegrounds : networkingForegrounds // ignore: cast_nullable_to_non_nullable -as List,toggleableForegrounds: null == toggleableForegrounds ? _self._toggleableForegrounds : toggleableForegrounds // ignore: cast_nullable_to_non_nullable -as Map>,cursor: null == cursor ? _self.cursor : cursor // ignore: cast_nullable_to_non_nullable -as MouseCursor,temporaryCursor: freezed == temporaryCursor ? _self.temporaryCursor : temporaryCursor // ignore: cast_nullable_to_non_nullable -as MouseCursor?,temporaryState: null == temporaryState ? _self.temporaryState : temporaryState // ignore: cast_nullable_to_non_nullable -as TemporaryState,lastPosition: freezed == lastPosition ? _self.lastPosition : lastPosition // ignore: cast_nullable_to_non_nullable -as Offset?,pointers: null == pointers ? _self._pointers : pointers // ignore: cast_nullable_to_non_nullable -as List,buttons: freezed == buttons ? _self.buttons : buttons // ignore: cast_nullable_to_non_nullable -as int?,location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable -as AssetLocation,embedding: freezed == embedding ? _self.embedding : embedding // ignore: cast_nullable_to_non_nullable -as Embedding?,saved: null == saved ? _self.saved : saved // ignore: cast_nullable_to_non_nullable -as SaveState,toolbar: freezed == toolbar ? _self.toolbar : toolbar // ignore: cast_nullable_to_non_nullable -as PreferredSizeWidget?,temporaryToolbar: freezed == temporaryToolbar ? _self.temporaryToolbar : temporaryToolbar // ignore: cast_nullable_to_non_nullable -as PreferredSizeWidget?,rendererStates: null == rendererStates ? _self._rendererStates : rendererStates // ignore: cast_nullable_to_non_nullable -as Map,temporaryRendererStates: freezed == temporaryRendererStates ? _self._temporaryRendererStates : temporaryRendererStates // ignore: cast_nullable_to_non_nullable -as Map?,viewOption: null == viewOption ? _self.viewOption : viewOption // ignore: cast_nullable_to_non_nullable -as ViewOption,hideUi: null == hideUi ? _self.hideUi : hideUi // ignore: cast_nullable_to_non_nullable -as HideState,areaNavigatorCreate: null == areaNavigatorCreate ? _self.areaNavigatorCreate : areaNavigatorCreate // ignore: cast_nullable_to_non_nullable -as bool,areaNavigatorExact: null == areaNavigatorExact ? _self.areaNavigatorExact : areaNavigatorExact // ignore: cast_nullable_to_non_nullable -as bool,areaNavigatorAsk: null == areaNavigatorAsk ? _self.areaNavigatorAsk : areaNavigatorAsk // ignore: cast_nullable_to_non_nullable -as bool,navigatorEnabled: null == navigatorEnabled ? _self.navigatorEnabled : navigatorEnabled // ignore: cast_nullable_to_non_nullable -as bool,navigatorPage: null == navigatorPage ? _self.navigatorPage : navigatorPage // ignore: cast_nullable_to_non_nullable -as NavigatorPage,isCreating: null == isCreating ? _self.isCreating : isCreating // ignore: cast_nullable_to_non_nullable -as bool,userName: null == userName ? _self.userName : userName // ignore: cast_nullable_to_non_nullable -as String,penDetected: null == penDetected ? _self.penDetected : penDetected // ignore: cast_nullable_to_non_nullable -as bool,sessionPenOnlyInput: null == sessionPenOnlyInput ? _self.sessionPenOnlyInput : sessionPenOnlyInput // ignore: cast_nullable_to_non_nullable -as bool, - )); -} - -/// Create a copy of CurrentIndex -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$CameraViewportCopyWith<$Res> get cameraViewport { - - return $CameraViewportCopyWith<$Res>(_self.cameraViewport, (value) { - return _then(_self.copyWith(cameraViewport: value)); - }); -}/// Create a copy of CurrentIndex -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$UtilitiesStateCopyWith<$Res> get utilities { - - return $UtilitiesStateCopyWith<$Res>(_self.utilities, (value) { - return _then(_self.copyWith(utilities: value)); - }); -}/// Create a copy of CurrentIndex -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$ViewOptionCopyWith<$Res> get viewOption { - - return $ViewOptionCopyWith<$Res>(_self.viewOption, (value) { - return _then(_self.copyWith(viewOption: value)); - }); -} -} - -// dart format on diff --git a/app/lib/cubits/editor_controller.dart b/app/lib/cubits/editor_controller.dart new file mode 100644 index 000000000000..de8a9172a96b --- /dev/null +++ b/app/lib/cubits/editor_controller.dart @@ -0,0 +1,145 @@ +import 'dart:async'; +import 'dart:math'; +import 'dart:ui' as ui; + +import 'package:butterfly/api/image.dart'; +import 'package:butterfly/bloc/document_bloc.dart'; +import 'package:butterfly/cubits/editor_session.dart'; +import 'package:butterfly/cubits/editor_runtime.dart'; +import 'package:butterfly/cubits/settings.dart'; +import 'package:butterfly/cubits/transform.dart'; +import 'package:butterfly/helpers/rect.dart'; +import 'package:butterfly/helpers/xml.dart'; +import 'package:butterfly/renderers/cursors/user.dart'; +import 'package:butterfly/renderers/renderer.dart'; +import 'package:butterfly/services/network.dart'; +import 'package:butterfly/services/logger.dart'; +import 'package:butterfly/views/navigator/constants.dart'; +import 'package:butterfly/views/navigator/view.dart'; +import 'package:butterfly/visualizer/tool.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:image/image.dart' as img; +import 'package:lw_file_system/lw_file_system.dart'; +import 'package:material_leap/material_leap.dart'; +import 'package:networker/networker.dart'; +import 'package:pdfrx/pdfrx.dart'; +import 'package:xml/xml.dart'; + +import '../embed/embedding.dart'; +import '../handlers/handler.dart'; +import '../models/viewport.dart'; +import '../view_painter.dart'; + +export 'editor_runtime.dart' + show + DocumentSaveCubit, + DocumentSaveState, + EditorInputCubit, + EditorInputState, + EditorViewCubit, + EditorViewState, + HideState, + RendererCubit, + RendererRuntimeState, + RendererState, + SaveState, + TemporaryState, + ToolCubit, + ToolRuntimeState; + +part 'editor_controller_methods.dart'; + +class EditorController { + final SettingsCubit settingsCubit; + final TransformCubit transformCubit; + final NetworkingService networkingService; + final EditorSessionCubit? editorSessionCubit; + late final RendererCubit rendererCubit; + late final ToolCubit toolCubit; + late final EditorInputCubit inputCubit; + late final DocumentSaveCubit saveCubit; + late final EditorViewCubit viewCubit; + StreamSubscription? _rendererSubscription; + StreamSubscription? _toolSubscription; + StreamSubscription? _inputSubscription; + StreamSubscription? _viewSubscription; + + EditorController( + this.settingsCubit, + this.transformCubit, + CameraViewport viewport, { + Embedding? embedding, + NetworkingService? networkingService, + this.editorSessionCubit, + bool absolute = false, + }) : networkingService = networkingService ?? NetworkingService(), + rendererCubit = RendererCubit( + settingsCubit, + RendererRuntimeState(cameraViewport: viewport), + ), + toolCubit = ToolCubit( + ToolRuntimeState( + index: editorSessionCubit?.state.selectedTool.toolIndex, + handler: HandHandler(), + ), + ), + inputCubit = EditorInputCubit(settingsCubit), + saveCubit = DocumentSaveCubit( + settingsCubit, + DocumentSaveState( + embedding: embedding, + saved: absolute ? SaveState.absoluteRead : SaveState.saved, + ), + ), + viewCubit = EditorViewCubit( + editorSessionCubit: editorSessionCubit, + initial: EditorViewState( + utilities: + editorSessionCubit?.state.utilities ?? const UtilitiesState(), + navigatorEnabled: + editorSessionCubit?.state.navigatorEnabled ?? false, + navigatorPage: + editorSessionCubit?.navigatorPage ?? NavigatorPage.waypoints, + areaNavigatorCreate: + editorSessionCubit?.state.areaNavigatorCreate ?? true, + areaNavigatorExact: + editorSessionCubit?.state.areaNavigatorExact ?? true, + areaNavigatorAsk: + editorSessionCubit?.state.areaNavigatorAsk ?? false, + ), + ) { + _previousRendererState = rendererCubit.state; + _previousToolState = toolCubit.state; + _previousInputState = inputCubit.state; + _previousViewState = viewCubit.state; + _transformSubscription = transformCubit.stream.listen(_onTransformChanged); + _rendererSubscription = rendererCubit.stream.listen(_onRendererChanged); + _toolSubscription = toolCubit.stream.listen(_onToolChanged); + _inputSubscription = inputCubit.stream.listen(_onInputChanged); + _viewSubscription = viewCubit.stream.listen(_onViewChanged); + } + + StreamSubscription? _transformSubscription; + Timer? _transformDebounceTimer; + Timer? _networkingDebounceTimer; + RendererRuntimeState _previousRendererState = const RendererRuntimeState(); + ToolRuntimeState? _previousToolState; + EditorInputState _previousInputState = const EditorInputState(); + EditorViewState? _previousViewState; + WeakReference? _documentBloc; + var _closed = false; + var _isClosing = false; + + bool get isClosed => _closed; + + Future reload(DocumentBloc bloc, [DocumentLoaded? blocState]) => + reloadRuntime(bloc, blocState); +} + +Future _toFile((NoteData, bool) args) async { + return args.$1.toFile(isTextBased: args.$2); +} diff --git a/app/lib/cubits/current_index.dart b/app/lib/cubits/editor_controller_methods.dart similarity index 70% rename from app/lib/cubits/current_index.dart rename to app/lib/cubits/editor_controller_methods.dart index 52c1ca92fabb..15e6128dd187 100644 --- a/app/lib/cubits/current_index.dart +++ b/app/lib/cubits/editor_controller_methods.dart @@ -1,144 +1,7 @@ -import 'dart:async'; -import 'dart:math'; -import 'dart:ui' as ui; - -import 'package:butterfly/api/image.dart'; -import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/settings.dart'; -import 'package:butterfly/cubits/transform.dart'; -import 'package:butterfly/helpers/async.dart'; -import 'package:butterfly/helpers/rect.dart'; -import 'package:butterfly/helpers/xml.dart'; -import 'package:butterfly/renderers/cursors/user.dart'; -import 'package:butterfly/renderers/renderer.dart'; -import 'package:butterfly/services/network.dart'; -import 'package:butterfly/services/logger.dart'; -import 'package:butterfly/views/navigator/constants.dart'; -import 'package:butterfly/views/navigator/view.dart'; -import 'package:butterfly/visualizer/tool.dart'; -import 'package:butterfly_api/butterfly_api.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:image/image.dart' as img; -import 'package:lw_file_system/lw_file_system.dart'; -import 'package:material_leap/material_leap.dart'; -import 'package:networker/networker.dart'; -import 'package:pdfrx/pdfrx.dart'; -import 'package:synchronized/synchronized.dart'; -import 'package:xml/xml.dart'; - -import '../embed/embedding.dart'; -import '../handlers/handler.dart'; -import '../models/viewport.dart'; -import '../selections/selection.dart'; -import '../theme.dart'; -import '../view_painter.dart'; - -part 'current_index.freezed.dart'; - -enum SaveState { saved, saving, unsaved, absoluteRead } - -enum HideState { visible, keyboard, touch } - -enum RendererState { visible, temporary, hidden } - -enum TemporaryState { allowClick, removeAfterClick, removeAfterRelease } - -@Freezed(equal: false) -sealed class CurrentIndex with _$CurrentIndex { - const CurrentIndex._(); - - const factory CurrentIndex( - int? index, - Handler handler, - CameraViewport cameraViewport, { - @Default(false) bool isSaveDelayed, - @Default(UtilitiesState()) UtilitiesState utilities, - Handler? temporaryHandler, - int? temporaryIndex, - @Default([]) List foregrounds, - Selection? selection, - @Default(false) bool pinned, - List? temporaryForegrounds, - @Default({}) Map> toggleableHandlers, - @Default([]) List networkingForegrounds, - @Default({}) Map> toggleableForegrounds, - @Default(MouseCursor.defer) MouseCursor cursor, - MouseCursor? temporaryCursor, - @Default(TemporaryState.allowClick) TemporaryState temporaryState, - Offset? lastPosition, - @Default([]) List pointers, - int? buttons, - @Default(AssetLocation(path: '')) AssetLocation location, - Embedding? embedding, - @Default(SaveState.saved) SaveState saved, - PreferredSizeWidget? toolbar, - PreferredSizeWidget? temporaryToolbar, - @Default({}) Map rendererStates, - @Default({}) Map? temporaryRendererStates, - @Default(ViewOption()) ViewOption viewOption, - @Default(HideState.visible) HideState hideUi, - @Default(true) bool areaNavigatorCreate, - @Default(true) bool areaNavigatorExact, - @Default(false) bool areaNavigatorAsk, - @Default(false) bool navigatorEnabled, - @Default(NavigatorPage.waypoints) NavigatorPage navigatorPage, - @Default(false) bool isCreating, - @Default('') String userName, - @Default(false) bool penDetected, - @Default(false) bool sessionPenOnlyInput, - }) = _CurrentIndex; - - bool get absolute => saved == SaveState.absoluteRead; - - MouseCursor get currentCursor => temporaryCursor ?? cursor; - - Map get allRendererStates => { - ...rendererStates, - ...?temporaryRendererStates, - }; - - List getAllForegrounds([bool networking = true]) => [ - ...foregrounds, - ...?temporaryForegrounds, - ...toggleableForegrounds.values.expand((e) => e), - if (networking) ...networkingForegrounds, - ]; -} +part of 'editor_controller.dart'; -class CurrentIndexCubit extends Cubit { - final SettingsCubit settingsCubit; - final TransformCubit transformCubit; - final NetworkingService networkingService; - - CurrentIndexCubit( - this.settingsCubit, - this.transformCubit, - CameraViewport viewport, { - Embedding? embedding, - NetworkingService? networkingService, - bool absolute = false, - }) : networkingService = networkingService ?? NetworkingService(), - super( - CurrentIndex( - null, - HandHandler(), - viewport, - embedding: embedding, - saved: absolute ? SaveState.absoluteRead : SaveState.saved, - ), - ) { - _transformSubscription = transformCubit.stream.listen(_onTransformChanged); - } - - StreamSubscription? _transformSubscription; - Timer? _transformDebounceTimer; - var _isClosing = false; - - static bool _sameRendererList( +extension EditorControllerMethods on EditorController { + bool _sameRendererList( List> a, List> b, ) { @@ -150,19 +13,6 @@ class CurrentIndexCubit extends Cubit { return true; } - /// Returns the effective pen-only input state. - /// If the setting is null (auto), uses the session-based state. - /// Otherwise uses the persisted setting. - bool get effectivePenOnlyInput { - final setting = settingsCubit.state.penOnlyInput; - if (setting != null) return setting; - return state.sessionPenOnlyInput; - } - - bool get moveEnabled => - (settingsCubit.state.inputGestures && state.pointers.length > 1) && - settingsCubit.state.moveOnGesture; - void _onTransformChanged(CameraTransform transform) { // Debounce transform changes to avoid excessive updates during pan/zoom _transformDebounceTimer?.cancel(); @@ -173,12 +23,13 @@ class CurrentIndexCubit extends Cubit { void _updateVisibleElements() { if (isClosed) return; - final unbaked = state.cameraViewport.unbakedElements; - final baked = state.cameraViewport.bakedElements; + final unbaked = rendererCubit.state.cameraViewport.unbakedElements; + final baked = rendererCubit.state.cameraViewport.bakedElements; final rect = getViewportRect(); - final currentVisible = state.cameraViewport.visibleElements; - final currentVisibleUnbaked = state.cameraViewport.visibleUnbakedElements; + final currentVisible = rendererCubit.state.cameraViewport.visibleElements; + final currentVisibleUnbaked = + rendererCubit.state.cameraViewport.visibleUnbakedElements; final visibleUnbaked = unbaked.where((e) => e.isVisible(rect)).toList(); final visible = >[ @@ -191,7 +42,7 @@ class CurrentIndexCubit extends Cubit { return; } - final newViewport = state.cameraViewport.withUnbaked( + final newViewport = rendererCubit.state.cameraViewport.withUnbaked( unbaked, visibleElements: visible, visibleUnbakedElements: visibleUnbaked, @@ -209,11 +60,9 @@ class CurrentIndexCubit extends Cubit { if (isClosed) return; - emit(state.copyWith(cameraViewport: newViewport)); + rendererCubit.setViewport(newViewport); } - WeakReference? _documentBloc; - DocumentBloc? get _activeDocumentBloc { final bloc = _documentBloc?.target; if (bloc == null || bloc.isClosed) return null; @@ -227,36 +76,16 @@ class CurrentIndexCubit extends Cubit { void init(DocumentBloc bloc) { _documentBloc = WeakReference(bloc); - changeTool(bloc, index: state.index ?? 0); + final blocState = bloc.state; + final index = blocState is DocumentLoadSuccess + ? editorSessionCubit?.resolveToolIndex(blocState.info) + : toolCubit.state.index; + changeTool(bloc, index: index ?? 0); networkingService.setup(bloc); } - void setPenDetected(bool detected) { - if (state.penDetected == detected) return; - // When pen is detected and setting is auto (null), enable session pen-only - final shouldEnableSessionPenOnly = - detected && - settingsCubit.state.penOnlyInput == null && - !state.sessionPenOnlyInput; - emit( - state.copyWith( - penDetected: detected, - sessionPenOnlyInput: shouldEnableSessionPenOnly - ? true - : state.sessionPenOnlyInput, - ), - ); - } - - void setSessionPenOnlyInput(bool value) { - if (state.sessionPenOnlyInput == value) return; - emit(state.copyWith(sessionPenOnlyInput: value)); - } - - final Set> _initializedElements = {}; - void invalidateRenderers(Iterable> renderers) { - _initializedElements.removeAll(renderers); + rendererCubit.initializedElements.removeAll(renderers); } Future _updateOnVisible( @@ -269,10 +98,10 @@ class CurrentIndexCubit extends Cubit { final nextVisibleSet = newVisibleList.toSet(); final newVisible = newVisibleList - .where((e) => !_initializedElements.contains(e)) + .where((e) => !rendererCubit.initializedElements.contains(e)) .toList(); - final newlyHidden = _initializedElements + final newlyHidden = rendererCubit.initializedElements .where((e) => !nextVisibleSet.contains(e)) .toList(); @@ -281,7 +110,7 @@ class CurrentIndexCubit extends Cubit { final transform = renderTransform ?? transformCubit.state; final size = targetSize ?? newViewport.toSize(); - _initializedElements.removeAll(newlyHidden); + rendererCubit.initializedElements.removeAll(newlyHidden); if (newVisible.isNotEmpty) { talker.verbose('Updating visible elements: ${newVisible.length} new'); @@ -302,7 +131,7 @@ class CurrentIndexCubit extends Cubit { return null; }), ); - _initializedElements.addAll(initialized.nonNulls); + rendererCubit.initializedElements.addAll(initialized.nonNulls); } if (newlyHidden.isNotEmpty) { @@ -315,20 +144,14 @@ class CurrentIndexCubit extends Cubit { } } - ThemeData getTheme( - bool dark, [ - VisualDensity? density, - ColorScheme? overridden, - ]) => getThemeData(settingsCubit.state.design, dark, density, overridden); - Handler getHandler({bool disableTemporary = false}) { - if (state.embedding?.editable == false) { + if (saveCubit.state.embedding?.editable == false) { return HandHandler(); } if (disableTemporary) { - return state.handler; + return toolCubit.state.handler; } else { - return state.temporaryHandler ?? state.handler; + return toolCubit.state.temporaryHandler ?? toolCubit.state.handler; } } @@ -340,15 +163,15 @@ class CurrentIndexCubit extends Cubit { bool allowBake = true, }) async { talker.verbose('Changing tool to index: $index'); - await resetInput(bloc); + await toolCubit.resetInput(bloc, inputCubit); final blocState = bloc.state; if (blocState is! DocumentLoadSuccess) return null; - if (state.embedding?.editable == false) { + if (saveCubit.state.embedding?.editable == false) { return null; } final document = blocState.data; final info = blocState.info; - index ??= state.index ?? 0; + index ??= toolCubit.state.index ?? 0; if (handler == null && (index < 0 || index >= info.tools.length)) { return null; } @@ -358,8 +181,8 @@ class CurrentIndexCubit extends Cubit { selectState = await handler.onSelected(context); } if (selectState != SelectState.none) { - state.handler.dispose(bloc); - state.temporaryHandler?.dispose(bloc); + toolCubit.state.handler.dispose(bloc); + toolCubit.state.temporaryHandler?.dispose(bloc); _disposeTemporaryForegrounds(); _disposeForegrounds(); final foregrounds = handler.createForegrounds( @@ -382,35 +205,30 @@ class CurrentIndexCubit extends Cubit { ); } if (selectState == SelectState.normal) { - emit( - state.copyWith( - index: index, - handler: handler, - cursor: handler.cursor ?? MouseCursor.defer, - foregrounds: foregrounds, - toolbar: await handler.getToolbar(bloc), - rendererStates: handler.rendererStates, - temporaryForegrounds: null, - temporaryHandler: null, - temporaryToolbar: null, - temporaryCursor: null, - temporaryRendererStates: null, - temporaryIndex: null, - ), + editorSessionCubit?.updateSelectedTool(handler.data, index); + toolCubit.setActiveTool( + index: index, + handler: handler, + cursor: handler.cursor ?? MouseCursor.defer, + foregrounds: foregrounds, + toolbar: await handler.getToolbar(bloc), + rendererStates: handler.rendererStates, + ); + rendererCubit.setRendererStates( + rendererStates: handler.rendererStates, + temporaryRendererStates: const {}, ); if (allowBake) await bake(blocState); } else { if (isHandlerEnabled(index)) { disableHandler(bloc, index); } else { - emit( - state.copyWith( - toggleableHandlers: {...state.toggleableHandlers, index: handler}, - toggleableForegrounds: { - ...state.toggleableForegrounds, - index: foregrounds, - }, - ), + toolCubit.setToggleable( + handlers: {...toolCubit.state.toggleableHandlers, index: handler}, + foregrounds: { + ...toolCubit.state.toggleableForegrounds, + index: foregrounds, + }, ); } } @@ -418,39 +236,63 @@ class CurrentIndexCubit extends Cubit { return handler; } - Timer? _networkingDebounceTimer; - - @override - void onChange(Change change) { - super.onChange(change); + void _onToolChanged(ToolRuntimeState next) { if (_isClosing) { return; } - final current = change.currentState; - final next = change.nextState; + final current = _previousToolState; + _previousToolState = next; + if (current == null) return; - // Debounce networking state updates to avoid flooding the network if (next.foregrounds != current.foregrounds || - next.temporaryForegrounds != current.temporaryForegrounds || - next.lastPosition != current.lastPosition || - next.userName != current.userName) { + next.temporaryForegrounds != current.temporaryForegrounds) { + _networkingDebounceTimer?.cancel(); + _networkingDebounceTimer = Timer(const Duration(milliseconds: 50), () { + if (!isClosed) _sendNetworkingState(); + }); + } + } + + void _onInputChanged(EditorInputState next) { + if (_isClosing) return; + final current = _previousInputState; + _previousInputState = next; + if (next.lastPosition != current.lastPosition) { + _networkingDebounceTimer?.cancel(); + _networkingDebounceTimer = Timer(const Duration(milliseconds: 50), () { + if (!isClosed) _sendNetworkingState(); + }); + } + } + + void _onViewChanged(EditorViewState next) { + if (_isClosing) return; + final current = _previousViewState; + _previousViewState = next; + if (current != null && next.userName != current.userName) { _networkingDebounceTimer?.cancel(); _networkingDebounceTimer = Timer(const Duration(milliseconds: 50), () { if (!isClosed) _sendNetworkingState(); }); } + } + void _onRendererChanged(RendererRuntimeState next) { + if (_isClosing) return; + final current = _previousRendererState; + _previousRendererState = next; final currentViewport = current.cameraViewport; final newViewport = next.cameraViewport; - // Only notify handlers if viewport actually changed if (!identical(currentViewport, newViewport) && currentViewport != newViewport) { - next.handler.onViewportUpdated(currentViewport, newViewport); - next.temporaryHandler?.onViewportUpdated(currentViewport, newViewport); + toolCubit.state.handler.onViewportUpdated(currentViewport, newViewport); + toolCubit.state.temporaryHandler?.onViewportUpdated( + currentViewport, + newViewport, + ); } - // Schedule image disposal if changed currentViewport.disposeImages(except: newViewport); } @@ -458,11 +300,11 @@ class CurrentIndexCubit extends Cubit { List>? foregrounds, Offset? cursor, }) { - cursor ??= state.lastPosition ?? Offset.zero; + cursor ??= inputCubit.state.lastPosition ?? Offset.zero; networkingService.sendUser( NetworkingUser( cursor: transformCubit.state.localToGlobal(cursor).toPoint(), - foreground: (foregrounds ?? state.getAllForegrounds(false)) + foreground: (foregrounds ?? toolCubit.state.getAllForegrounds(false)) .map((e) => e.element) .whereType() .toList(), @@ -484,7 +326,7 @@ class CurrentIndexCubit extends Cubit { .expand((entry) => entry.value.foreground ?? const []) .toSet(); - final foregrounds = state.networkingForegrounds.toList(); + final foregrounds = toolCubit.state.networkingForegrounds.toList(); foregrounds.removeWhere((renderer) { bool shouldRemove; if (renderer is UserCursor) { @@ -531,34 +373,36 @@ class CurrentIndexCubit extends Cubit { ), ); foregrounds.addAll(added); - emit(state.copyWith(networkingForegrounds: foregrounds)); + toolCubit.setForegrounds(networkingForegrounds: foregrounds); } void updateLastPosition(Offset position) { // Only emit if position changed by more than 1 pixel to reduce state updates - final lastPos = state.lastPosition; + final lastPos = inputCubit.state.lastPosition; if (lastPos != null) { final dx = (position.dx - lastPos.dx).abs(); final dy = (position.dy - lastPos.dy).abs(); if (dx < 1 && dy < 1) return; } - emit(state.copyWith(lastPosition: position)); + inputCubit.updateLastPosition(position); } - Future updateHandler(DocumentBloc bloc, Handler handler) async => emit( - state.copyWith( - handler: handler, - cursor: handler.cursor ?? MouseCursor.defer, - toolbar: await handler.getToolbar(bloc), - rendererStates: handler.rendererStates, - ), - ); + Future updateHandler(DocumentBloc bloc, Handler handler) async { + toolCubit.replace( + toolCubit.state.copyWith( + handler: handler, + cursor: handler.cursor ?? MouseCursor.defer, + toolbar: await handler.getToolbar(bloc), + ), + ); + rendererCubit.setRendererStates(rendererStates: handler.rendererStates); + } Future updateTool(DocumentBloc bloc, Tool tool) async { talker.verbose('Updating tool: ${tool.runtimeType}'); final docState = bloc.state; if (docState is! DocumentLoadSuccess) return; - state.handler.dispose(bloc); + toolCubit.state.handler.dispose(bloc); final handler = Handler.fromTool(tool); _disposeForegrounds(); final foregrounds = handler.createForegrounds( @@ -580,23 +424,22 @@ class CurrentIndexCubit extends Cubit { ), ); } - emit( - state.copyWith( - index: state.index, - handler: handler, - foregrounds: foregrounds, - toolbar: await handler.getToolbar(bloc), - rendererStates: handler.rendererStates, - cursor: handler.cursor ?? MouseCursor.defer, - ), + toolCubit.setActiveTool( + index: toolCubit.state.index, + handler: handler, + cursor: handler.cursor ?? MouseCursor.defer, + foregrounds: foregrounds, + toolbar: await handler.getToolbar(bloc), + rendererStates: handler.rendererStates, ); + rendererCubit.setRendererStates(rendererStates: handler.rendererStates); } Future updateTemporaryTool(DocumentBloc bloc, Tool tool) async { talker.verbose('Updating temporary tool: ${tool.runtimeType}'); final docState = bloc.state; if (docState is! DocumentLoadSuccess) return; - state.temporaryHandler?.dispose(bloc); + toolCubit.state.temporaryHandler?.dispose(bloc); final handler = Handler.fromTool(tool); _disposeTemporaryForegrounds(); final foregrounds = handler.createForegrounds( @@ -618,14 +461,16 @@ class CurrentIndexCubit extends Cubit { ), ); } - emit( - state.copyWith( - temporaryHandler: handler, - temporaryForegrounds: foregrounds, - temporaryToolbar: await handler.getToolbar(bloc), - temporaryRendererStates: handler.rendererStates, - temporaryCursor: handler.cursor, - ), + toolCubit.setTemporaryTool( + handler: handler, + index: toolCubit.state.temporaryIndex, + foregrounds: foregrounds, + toolbar: await handler.getToolbar(bloc), + cursor: handler.cursor, + rendererStates: handler.rendererStates, + ); + rendererCubit.setRendererStates( + temporaryRendererStates: handler.rendererStates, ); } @@ -636,25 +481,27 @@ class CurrentIndexCubit extends Cubit { } void _disposeForegrounds() { - for (final r in state.foregrounds) { + for (final r in toolCubit.state.foregrounds) { r.dispose(); } } void _disposeTemporaryForegrounds() { - for (final r in state.temporaryForegrounds ?? []) { + for (final r in toolCubit.state.temporaryForegrounds ?? []) { r.dispose(); } } void _disposeNetworkingForegrounds() { - for (final r in state.networkingForegrounds) { + for (final r in toolCubit.state.networkingForegrounds) { r.dispose(); } } void _disposeToggleableForegrounds() { - for (final r in state.toggleableForegrounds.values.expand((e) => e)) { + for (final r in toolCubit.state.toggleableForegrounds.values.expand( + (e) => e, + )) { r.dispose(); } } @@ -673,10 +520,10 @@ class CurrentIndexCubit extends Cubit { ) { Handler? handler; bool needsDispose = false; - if (state.index == index) { + if (toolCubit.state.index == index) { handler = fetchHandler>(disableTemporary: true); - } else if (state.toggleableHandlers.containsKey(index)) { - handler = state.toggleableHandlers[index]; + } else if (toolCubit.state.toggleableHandlers.containsKey(index)) { + handler = toolCubit.state.toggleableHandlers[index]; } if (handler == null) { List tools = const []; @@ -701,7 +548,7 @@ class CurrentIndexCubit extends Cubit { DocumentLoaded blocState, { bool allowBake = true, }) async { - talker.verbose('Refreshing CurrentIndexCubit'); + talker.verbose('Refreshing EditorController'); final document = blocState.data; final page = blocState.page; final info = blocState.info; @@ -710,15 +557,10 @@ class CurrentIndexCubit extends Cubit { const mapEq = MapEquality(); if (!isClosed) { _disposeAllForegrounds(); - final temporaryForegrounds = state.temporaryHandler?.createForegrounds( - this, - document, - page, - info, - currentArea, - ); + final temporaryForegrounds = toolCubit.state.temporaryHandler + ?.createForegrounds(this, document, page, info, currentArea); if (temporaryForegrounds != null && - state.temporaryHandler?.setupForegrounds == true) { + toolCubit.state.temporaryHandler?.setupForegrounds == true) { await Future.wait( temporaryForegrounds.map( (e) async => @@ -726,14 +568,14 @@ class CurrentIndexCubit extends Cubit { ), ); } - final foregrounds = state.handler.createForegrounds( + final foregrounds = toolCubit.state.handler.createForegrounds( this, document, page, info, currentArea, ); - if (state.handler.setupForegrounds) { + if (toolCubit.state.handler.setupForegrounds) { await Future.wait( foregrounds.map( (e) async => @@ -742,7 +584,7 @@ class CurrentIndexCubit extends Cubit { ); } final toggleableForegrounds = >{}; - for (final entry in state.toggleableHandlers.entries) { + for (final entry in toolCubit.state.toggleableHandlers.entries) { final handler = entry.value; final index = entry.key; final foregrounds = handler.createForegrounds( @@ -762,31 +604,43 @@ class CurrentIndexCubit extends Cubit { } toggleableForegrounds[index] = foregrounds; } - final rendererStates = state.handler.rendererStates; - final temporaryRendererStates = state.temporaryHandler?.rendererStates; - final statesChanged = !mapEq.equals(state.rendererStates, rendererStates); + final rendererStates = toolCubit.state.handler.rendererStates; + final temporaryRendererStates = + toolCubit.state.temporaryHandler?.rendererStates; + final statesChanged = !mapEq.equals( + rendererCubit.state.rendererStates, + rendererStates, + ); final temporaryStatesChanged = !mapEq.equals( - state.temporaryRendererStates, + rendererCubit.state.temporaryRendererStates, temporaryRendererStates, ); final shouldBake = statesChanged || temporaryStatesChanged; - emit( - state.copyWith( - temporaryForegrounds: temporaryForegrounds, - toggleableForegrounds: toggleableForegrounds, - foregrounds: foregrounds, - cursor: state.handler.cursor ?? MouseCursor.defer, - temporaryCursor: state.temporaryHandler?.cursor, - rendererStates: statesChanged ? rendererStates : state.rendererStates, - temporaryRendererStates: temporaryStatesChanged - ? temporaryRendererStates - : state.temporaryRendererStates, - ), + toolCubit.setForegrounds( + temporaryForegrounds: temporaryForegrounds, + toggleableForegrounds: toggleableForegrounds, + foregrounds: foregrounds, + cursor: toolCubit.state.handler.cursor ?? MouseCursor.defer, + temporaryCursor: toolCubit.state.temporaryHandler?.cursor, + rendererStates: statesChanged + ? rendererStates + : rendererCubit.state.rendererStates, + temporaryRendererStates: temporaryStatesChanged + ? temporaryRendererStates + : rendererCubit.state.temporaryRendererStates, + ); + rendererCubit.setRendererStates( + rendererStates: statesChanged + ? rendererStates + : rendererCubit.state.rendererStates, + temporaryRendererStates: temporaryStatesChanged + ? temporaryRendererStates + : rendererCubit.state.temporaryRendererStates, ); if (allowBake) { if (shouldBake) { return bake(blocState, reset: true); - } else if (!state.cameraViewport.baked) { + } else if (!rendererCubit.state.cameraViewport.baked) { return delayedBake(blocState); } } @@ -795,18 +649,21 @@ class CurrentIndexCubit extends Cubit { Future refreshToolbar(DocumentBloc bloc) async { if (!isClosed) { - final toolbar = await state.handler.getToolbar(bloc); - final temporaryToolbar = await state.temporaryHandler?.getToolbar(bloc); - emit( - state.copyWith(toolbar: toolbar, temporaryToolbar: temporaryToolbar), + final toolbar = await toolCubit.state.handler.getToolbar(bloc); + final temporaryToolbar = await toolCubit.state.temporaryHandler + ?.getToolbar(bloc); + toolCubit.setToolbar( + toolbar: toolbar, + temporaryToolbar: temporaryToolbar, ); } } /// Lightweight refresh that only updates foregrounds without rebaking. /// Use this when handler internal state changes but document hasn't changed. - Future refreshForegrounds(DocumentLoaded blocState) => - _foregroundRefreshRunner.schedule(() => _refreshForegrounds(blocState)); + Future refreshForegrounds(DocumentLoaded blocState) => toolCubit + .foregroundRefreshRunner + .schedule(() => _refreshForegrounds(blocState)); Future _refreshForegrounds(DocumentLoaded blocState) async { if (isClosed) return; @@ -819,16 +676,11 @@ class CurrentIndexCubit extends Cubit { _disposeForegrounds(); _disposeTemporaryForegrounds(); - final temporaryForegrounds = state.temporaryHandler?.createForegrounds( - this, - document, - page, - info, - currentArea, - ); + final temporaryForegrounds = toolCubit.state.temporaryHandler + ?.createForegrounds(this, document, page, info, currentArea); if (temporaryForegrounds != null && temporaryForegrounds.isNotEmpty && - state.temporaryHandler?.setupForegrounds == true) { + toolCubit.state.temporaryHandler?.setupForegrounds == true) { await Future.wait( temporaryForegrounds.map( (e) async => @@ -837,14 +689,14 @@ class CurrentIndexCubit extends Cubit { ); } - final foregrounds = state.handler.createForegrounds( + final foregrounds = toolCubit.state.handler.createForegrounds( this, document, page, info, currentArea, ); - if (foregrounds.isNotEmpty && state.handler.setupForegrounds) { + if (foregrounds.isNotEmpty && toolCubit.state.handler.setupForegrounds) { await Future.wait( foregrounds.map( (e) async => @@ -855,25 +707,37 @@ class CurrentIndexCubit extends Cubit { // Check if rendererStates changed and need a bake const mapEq = MapEquality(); - final rendererStates = state.handler.rendererStates; - final temporaryRendererStates = state.temporaryHandler?.rendererStates; - final statesChanged = !mapEq.equals(state.rendererStates, rendererStates); + final rendererStates = toolCubit.state.handler.rendererStates; + final temporaryRendererStates = + toolCubit.state.temporaryHandler?.rendererStates; + final statesChanged = !mapEq.equals( + rendererCubit.state.rendererStates, + rendererStates, + ); final temporaryStatesChanged = !mapEq.equals( - state.temporaryRendererStates, + rendererCubit.state.temporaryRendererStates, temporaryRendererStates, ); - emit( - state.copyWith( - foregrounds: foregrounds, - temporaryForegrounds: temporaryForegrounds, - cursor: state.handler.cursor ?? MouseCursor.defer, - temporaryCursor: state.temporaryHandler?.cursor, - rendererStates: statesChanged ? rendererStates : state.rendererStates, - temporaryRendererStates: temporaryStatesChanged - ? temporaryRendererStates - : state.temporaryRendererStates, - ), + toolCubit.setForegrounds( + foregrounds: foregrounds, + temporaryForegrounds: temporaryForegrounds, + cursor: toolCubit.state.handler.cursor ?? MouseCursor.defer, + temporaryCursor: toolCubit.state.temporaryHandler?.cursor, + rendererStates: statesChanged + ? rendererStates + : rendererCubit.state.rendererStates, + temporaryRendererStates: temporaryStatesChanged + ? temporaryRendererStates + : rendererCubit.state.temporaryRendererStates, + ); + rendererCubit.setRendererStates( + rendererStates: statesChanged + ? rendererStates + : rendererCubit.state.rendererStates, + temporaryRendererStates: temporaryStatesChanged + ? temporaryRendererStates + : rendererCubit.state.temporaryRendererStates, ); // If renderer states changed, we need to bake to hide/show original elements @@ -885,30 +749,13 @@ class CurrentIndexCubit extends Cubit { /// Ultra-lightweight update for cursor changes only. /// Use this when only the cursor appearance needs to change. void updateCursor(MouseCursor cursor) { - if (state.cursor != cursor) { - emit(state.copyWith(cursor: cursor)); + if (toolCubit.state.cursor != cursor) { + toolCubit.setCursor(cursor); } } - Tool? getTool(DocumentInfo info) { - var index = state.index; - if (index == null) { - return null; - } - if (info.tools.isEmpty || index < 0 || index >= info.tools.length) { - return null; - } - return info.tools[index]; - } - - T? fetchTool(DocumentInfo info) { - final tool = getTool(info); - if (tool is T) return tool; - return null; - } - Future toggleHandler(DocumentBloc bloc, int index) async { - if (state.toggleableHandlers.containsKey(index)) { + if (toolCubit.state.toggleableHandlers.containsKey(index)) { disableHandler(bloc, index); } else { await enableHandler(bloc, index); @@ -946,94 +793,53 @@ class CurrentIndexCubit extends Cubit { ), ); } - emit( - state.copyWith( - toggleableHandlers: Map.from(state.toggleableHandlers) - ..[index] = handler, - toggleableForegrounds: Map.from(state.toggleableForegrounds) - ..[index] = foregrounds, - ), + toolCubit.setToggleable( + handlers: Map.from(toolCubit.state.toggleableHandlers)..[index] = handler, + foregrounds: Map.from(toolCubit.state.toggleableForegrounds) + ..[index] = foregrounds, ); return handler; } bool disableHandler(DocumentBloc bloc, int index) { - final handler = state.toggleableHandlers[index]; + final handler = toolCubit.state.toggleableHandlers[index]; if (handler == null) { return false; } handler.dispose(bloc); final foregrounds = Map>.from( - state.toggleableForegrounds, + toolCubit.state.toggleableForegrounds, ); final current = foregrounds.remove(index); for (final r in current ?? []) { r.dispose(); } - emit( - state.copyWith( - toggleableHandlers: Map.from(state.toggleableHandlers)..remove(index), - toggleableForegrounds: foregrounds, - ), + toolCubit.setToggleable( + handlers: Map.from(toolCubit.state.toggleableHandlers)..remove(index), + foregrounds: foregrounds, ); return true; } bool isHandlerEnabled(int index) => - state.toggleableHandlers.containsKey(index); + toolCubit.state.toggleableHandlers.containsKey(index); void reset(DocumentBloc bloc) { - for (final r in renderers) { + for (final r in rendererCubit.renderers) { r.dispose(); } - _initializedElements.clear(); - state.handler.dispose(bloc); - state.temporaryHandler?.dispose(bloc); - for (var e in state.toggleableHandlers.values) { + rendererCubit.initializedElements.clear(); + toolCubit.state.handler.dispose(bloc); + toolCubit.state.temporaryHandler?.dispose(bloc); + for (var e in toolCubit.state.toggleableHandlers.values) { e.dispose(bloc); } _disposeForegrounds(); _disposeTemporaryForegrounds(); _disposeNetworkingForegrounds(); _disposeToggleableForegrounds(); - emit( - state.copyWith( - index: null, - handler: HandHandler(), - cursor: MouseCursor.defer, - foregrounds: const [], - temporaryHandler: null, - temporaryForegrounds: null, - temporaryCursor: null, - temporaryRendererStates: null, - toolbar: null, - temporaryToolbar: null, - temporaryIndex: null, - rendererStates: const {}, - toggleableHandlers: const >{}, - toggleableForegrounds: const >{}, - networkingForegrounds: const [], - cameraViewport: CameraViewport.unbaked(), - ), - ); - } - - void changeIndex(int i) { - emit(state.copyWith(index: i)); - } - - void addPointer(int pointer) { - final pointers = state.pointers; - if (pointers.contains(pointer)) return; - emit(state.copyWith(pointers: [...pointers, pointer])); - } - - void removePointer(int pointer) { - final pointers = state.pointers; - if (!pointers.contains(pointer)) return; - emit( - state.copyWith(pointers: pointers.where((p) => p != pointer).toList()), - ); + toolCubit.resetRuntime(); + rendererCubit.replace(const RendererRuntimeState()); } Future changeTemporaryHandlerIndex( @@ -1050,8 +856,10 @@ class CurrentIndexCubit extends Cubit { return null; } final tool = blocState.info.tools[index]; - final temporaryHandler = state.temporaryHandler; - if (!force && index == state.temporaryIndex && temporaryHandler != null) { + final temporaryHandler = toolCubit.state.temporaryHandler; + if (!force && + index == toolCubit.state.temporaryIndex && + temporaryHandler != null) { return temporaryHandler; } return changeTemporaryHandler( @@ -1077,7 +885,7 @@ class CurrentIndexCubit extends Cubit { final document = blocState.data; final page = blocState.page; final currentArea = blocState.currentArea; - state.temporaryHandler?.dispose(bloc); + toolCubit.state.temporaryHandler?.dispose(bloc); final selectState = await handler.onSelected(context); if (selectState == SelectState.normal) { @@ -1101,16 +909,17 @@ class CurrentIndexCubit extends Cubit { ), ); } - emit( - state.copyWith( - temporaryHandler: handler, - temporaryForegrounds: temporaryForegrounds, - temporaryToolbar: await handler.getToolbar(bloc), - temporaryCursor: handler.cursor, - temporaryRendererStates: handler.rendererStates, - temporaryState: temporaryState, - temporaryIndex: index, - ), + toolCubit.setTemporaryTool( + handler: handler, + index: index, + foregrounds: temporaryForegrounds, + toolbar: await handler.getToolbar(bloc), + cursor: handler.cursor, + rendererStates: handler.rendererStates, + temporaryState: temporaryState, + ); + rendererCubit.setRendererStates( + temporaryRendererStates: handler.rendererStates, ); await bake(blocState); } else if (selectState == SelectState.toggle && index != null) { @@ -1120,7 +929,7 @@ class CurrentIndexCubit extends Cubit { } void resetReleaseHandler(DocumentBloc bloc) { - if (state.temporaryState == TemporaryState.removeAfterRelease) { + if (toolCubit.state.temporaryState == TemporaryState.removeAfterRelease) { resetTemporaryHandler(bloc, true); } } @@ -1130,38 +939,34 @@ class CurrentIndexCubit extends Cubit { } void resetTemporaryHandler(DocumentBloc bloc, [bool force = false]) { - if (state.temporaryHandler == null) { + if (toolCubit.state.temporaryHandler == null) { return; } - if (!force && state.temporaryState != TemporaryState.removeAfterClick) { - if (state.temporaryState == TemporaryState.allowClick) { - emit(state.copyWith(temporaryState: TemporaryState.removeAfterClick)); + if (!force && + toolCubit.state.temporaryState != TemporaryState.removeAfterClick) { + if (toolCubit.state.temporaryState == TemporaryState.allowClick) { + toolCubit.setTemporaryState(TemporaryState.removeAfterClick); } return; } - state.temporaryHandler?.dispose(bloc); + toolCubit.state.temporaryHandler?.dispose(bloc); _disposeTemporaryForegrounds(); - emit( - state.copyWith( - temporaryHandler: null, - temporaryIndex: null, - temporaryForegrounds: null, - temporaryToolbar: null, - temporaryCursor: null, - temporaryRendererStates: null, - ), + toolCubit.setTemporaryTool( + handler: null, + index: null, + foregrounds: null, + toolbar: null, + cursor: null, + rendererStates: null, ); + rendererCubit.setRendererStates(temporaryRendererStates: const {}); } - List> get renderers => - List.from(state.cameraViewport.bakedElements) - ..addAll(state.cameraViewport.unbakedElements); - Renderer? getRenderer(PadElement element) => - renderers.firstWhereOrNull((renderer) => renderer.element == element); + rendererCubit.getRenderer(element); Rect getViewportRect({Size? viewportSize}) { - var size = viewportSize ?? state.cameraViewport.toSize(); + var size = viewportSize ?? rendererCubit.state.cameraViewport.toSize(); final transform = transformCubit.state; final resolution = settingsCubit.state.renderResolution; @@ -1229,12 +1034,6 @@ class CurrentIndexCubit extends Cubit { ); } - final _bakeLock = Lock(); - final _delayedBakeRunner = CoalescedAsyncRunner( - delay: const Duration(milliseconds: 100), - ); - final _foregroundRefreshRunner = CoalescedAsyncRunner(delay: Duration.zero); - bool _rectContains(Rect outer, Rect inner) { const tolerance = precisionErrorTolerance; return outer.left <= inner.left + tolerance && @@ -1249,9 +1048,9 @@ class CurrentIndexCubit extends Cubit { double? pixelRatio, bool reset = false, bool resetAllLayers = false, - }) => _bakeLock.synchronized(() async { + }) => rendererCubit.bakeLock.synchronized(() async { if (isClosed) return; - var cameraViewport = state.cameraViewport; + var cameraViewport = rendererCubit.state.cameraViewport; final startTransform = transformCubit.state; final startViewport = cameraViewport; final resolution = settingsCubit.state.renderResolution; @@ -1264,7 +1063,7 @@ class CurrentIndexCubit extends Cubit { size /= resolution.multiplier; } var transform = transformCubit.state; - var renderers = List>.from(this.renderers); + var renderers = List>.from(rendererCubit.renderers); final recorder = ui.PictureRecorder(); final canvas = ui.Canvas(recorder); final rect = getViewportRect(viewportSize: size); @@ -1275,7 +1074,7 @@ class CurrentIndexCubit extends Cubit { final info = blocState.info; final imageWidth = (size.width * ratio).ceil(); final imageHeight = (size.height * ratio).ceil(); - var allRendererStates = state.allRendererStates; + var allRendererStates = rendererCubit.state.allRendererStates; final rendererStatesChanged = !mapEquals( allRendererStates, cameraViewport.rendererStates, @@ -1462,7 +1261,9 @@ class CurrentIndexCubit extends Cubit { .toSet(); final newlyUnbaked = - (reset ? this.renderers : state.cameraViewport.unbakedElements) + (reset + ? rendererCubit.renderers + : rendererCubit.state.cameraViewport.unbakedElements) .where( (element) => !bakedElementsSet.contains(element.element) && @@ -1475,7 +1276,7 @@ class CurrentIndexCubit extends Cubit { // If state changed while baking (e.g. fast move submitted a newer viewport), // this bake output is stale and must not overwrite the latest viewport. - final currentViewport = state.cameraViewport; + final currentViewport = rendererCubit.state.cameraViewport; final currentTransform = transformCubit.state; if (!identical(currentViewport, startViewport) || currentTransform != startTransform) { @@ -1522,7 +1323,7 @@ class CurrentIndexCubit extends Cubit { rendererStates: allRendererStates, invisibleLayers: invisibleLayers, ); - emit(state.copyWith(cameraViewport: newViewport)); + rendererCubit.setViewport(newViewport); }); Future renderImage( @@ -1546,7 +1347,9 @@ class CurrentIndexCubit extends Cubit { canvas.scale(options.quality); final viewport = cameraViewport ?? - state.cameraViewport.unbake(unbakedElements: renderers); + rendererCubit.state.cameraViewport.unbake( + unbakedElements: rendererCubit.renderers, + ); final transform = CameraTransform( options.quality, Offset(options.x, options.y), @@ -1562,7 +1365,9 @@ class CurrentIndexCubit extends Cubit { ); for (final renderer in viewport.unbakedElements) { if (renderer.isVisible(exportRect)) { - final wasInitialized = _initializedElements.contains(renderer); + final wasInitialized = rendererCubit.initializedElements.contains( + renderer, + ); if (!wasInitialized) { await renderer.onVisible(this, docState, transform, size); hiddenRenderers.add(renderer); @@ -1647,11 +1452,11 @@ class CurrentIndexCubit extends Cubit { options.height.toDouble(), ); if (options.renderBackground) { - for (final e in state.cameraViewport.backgrounds) { + for (final e in rendererCubit.state.cameraViewport.backgrounds) { e.buildSvg(xml, document, page, rect); } } - for (var e in renderers) { + for (var e in rendererCubit.renderers) { if ((invisibleLayers?.contains(e.layer) ?? false) || !e.isVisible(rect)) { continue; } @@ -1665,8 +1470,8 @@ class CurrentIndexCubit extends Cubit { List>? backgrounds, List>? unbakedElements, }) async { - final elementsToCheck = unbakedElements ?? renderers; - final oldViewport = state.cameraViewport; + final elementsToCheck = unbakedElements ?? rendererCubit.renderers; + final oldViewport = rendererCubit.state.cameraViewport; final newViewport = oldViewport.unbake( unbakedElements: unbakedElements, visibleElements: elementsToCheck @@ -1675,7 +1480,7 @@ class CurrentIndexCubit extends Cubit { backgrounds: backgrounds, ); await _updateOnVisible(newViewport, blocState); - emit(state.copyWith(cameraViewport: newViewport)); + rendererCubit.setViewport(newViewport); } Future replaceUnbaked( @@ -1686,14 +1491,14 @@ class CurrentIndexCubit extends Cubit { final visibleElements = unbakedElements .where((e) => e.isVisible(getViewportRect())) .toList(); - final newViewport = state.cameraViewport.replaceUnbaked( + final newViewport = rendererCubit.state.cameraViewport.replaceUnbaked( unbakedElements, visibleElements: visibleElements, visibleUnbakedElements: visibleElements, backgrounds: backgrounds, ); await _updateOnVisible(newViewport, blocState); - emit(state.copyWith(cameraViewport: newViewport)); + rendererCubit.setViewport(newViewport); } Future loadElements( @@ -1704,10 +1509,10 @@ class CurrentIndexCubit extends Cubit { final document = docState.data; final assetService = docState.assetService; final page = docState.page; - var existing = renderers; + var existing = rendererCubit.renderers; if (reset) { for (var e in existing) { - _initializedElements.remove(e); + rendererCubit.initializedElements.remove(e); e.dispose(); } existing = []; @@ -1740,7 +1545,7 @@ class CurrentIndexCubit extends Cubit { ) .toList(); for (final e in dropped) { - _initializedElements.remove(e); + rendererCubit.initializedElements.remove(e); e.dispose(); } final newRenderers = elements @@ -1784,19 +1589,17 @@ class CurrentIndexCubit extends Cubit { ); final rect = getViewportRect(); final visibleElements = combined.where((e) => e.isVisible(rect)).toList(); - final oldViewport = state.cameraViewport; + final oldViewport = rendererCubit.state.cameraViewport; final newViewport = oldViewport.unbake( unbakedElements: combined, visibleElements: visibleElements, backgrounds: backgrounds, ); await _updateOnVisible(newViewport, docState); - emit( - state.copyWith( - location: state.embedding?.location ?? state.location, - cameraViewport: newViewport, - ), + saveCubit.setSaveState( + location: saveCubit.state.embedding?.location ?? saveCubit.state.location, ); + rendererCubit.setViewport(newViewport); } Future addUnbaked( @@ -1809,22 +1612,22 @@ class CurrentIndexCubit extends Cubit { .where((e) => e.isVisible(rect)) .toList(); final nextUnbaked = [ - ...state.cameraViewport.unbakedElements, + ...rendererCubit.state.cameraViewport.unbakedElements, ...unbakedElements, ]; - final newViewport = state.cameraViewport.withUnbaked( + final newViewport = rendererCubit.state.cameraViewport.withUnbaked( nextUnbaked, visibleElements: [ - ...state.cameraViewport.visibleElements, + ...rendererCubit.state.cameraViewport.visibleElements, ...visibleElements, ], visibleUnbakedElements: [ - ...state.cameraViewport.visibleUnbakedElements, + ...rendererCubit.state.cameraViewport.visibleUnbakedElements, ...visibleElements, ], ); await _updateOnVisible(newViewport, blocState); - emit(state.copyWith(cameraViewport: newViewport)); + rendererCubit.setViewport(newViewport); } void setSaveState({ @@ -1833,14 +1636,12 @@ class CurrentIndexCubit extends Cubit { bool absolute = false, bool? isCreating, bool keepRead = false, - }) => emit( - state.copyWith( - location: location ?? state.location, - isCreating: isCreating ?? state.isCreating, - saved: (absolute || (keepRead && state.absolute)) - ? SaveState.absoluteRead - : saved ?? state.saved, - ), + }) => saveCubit.setSaveState( + location: location, + saved: saved, + absolute: absolute, + isCreating: isCreating, + keepRead: keepRead, ); Future renderPDF( @@ -1923,96 +1724,25 @@ class CurrentIndexCubit extends Cubit { final docState = bloc.state; if (docState is! DocumentLoadSuccess) return; final info = docState.info; - final index = info.tools.indexOf(state.handler.data); + final index = info.tools.indexOf(toolCubit.state.handler.data); if (index < 0) { - changeTool(bloc, index: state.index ?? 0); - } - if (index == state.index) { - return; - } - changeIndex(index); - final selection = state.selection; - if (selection?.selected.contains(state.handler.data) ?? false) { - resetSelection(); + changeTool(bloc, index: toolCubit.state.index ?? 0); } - } - - void insertSelection(dynamic selected, [bool toggle = true]) { - final selection = state.selection; - if (selection == null) { - emit(state.copyWith(selection: Selection.from(selected))); - return; - } - Selection? next; - if (selection.selected.contains(selected) && toggle) { - if (selection.selected.length != 1) { - next = selection.remove(selected); - } - } else { - next = selection.insert(selected); - } - emit(state.copyWith(selection: next)); - } - - void changeSelection(dynamic selected, [bool toggle = true]) { - Selection? selection; - if (selected is Selection?) { - selection = selected; - } else if (!toggle || - !(state.selection?.selected.contains(selected) ?? false)) { - selection = Selection.from(selected); - } - emit(state.copyWith(selection: selection)); - } - - void removeSelection(List selected) { - Selection? selection = state.selection; - if (selection == null) { + if (index == toolCubit.state.index) { return; } - for (final s in selected) { - selection = selection?.remove(s); + toolCubit.setIndex(index); + final selection = toolCubit.state.selection; + if (selection?.selected.contains(toolCubit.state.handler.data) ?? false) { + toolCubit.resetSelection(); } - emit(state.copyWith(selection: selection)); - } - - void resetSelection({bool force = false}) { - if (force || !state.pinned) { - emit(state.copyWith(selection: null)); - } - } - - void setButtons(int buttons) { - emit(state.copyWith(buttons: buttons)); - } - - void removeButtons() { - emit(state.copyWith(buttons: null)); - } - - Future resetInput(DocumentBloc bloc) async { - await state.handler.resetInput(bloc); - emit(state.copyWith(buttons: null, pointers: [])); - } - - void changeTemporaryHandlerMove() { - emit( - state.copyWith( - temporaryHandler: HandHandler(), - temporaryIndex: null, - temporaryCursor: null, - temporaryRendererStates: null, - temporaryForegrounds: null, - temporaryToolbar: null, - ), - ); } Rect getContentRect([Area? currentArea]) { if (currentArea != null) { return currentArea.rect; } - final renderers = this.renderers; + final renderers = rendererCubit.renderers; if (renderers.isEmpty) { return Rect.zero; } @@ -2022,7 +1752,7 @@ class CurrentIndexCubit extends Cubit { var maxX = double.negativeInfinity; var maxY = double.negativeInfinity; - for (final renderer in renderers) { + for (final renderer in rendererCubit.renderers) { final rect = renderer.expandedRect; if (rect != null) { minX = min(minX, rect.left); @@ -2050,29 +1780,12 @@ class CurrentIndexCubit extends Cubit { ); } - Future updateUtilities({ - UtilitiesState? utilities, - ViewOption? view, - }) async { - var state = this.state; - state = state.copyWith( - utilities: utilities ?? state.utilities, - viewOption: view ?? state.viewOption, - ); - emit(state); - if (utilities != null) { - return settingsCubit.changeUtilities(utilities); - } - } - - void togglePin() => emit(state.copyWith(pinned: !state.pinned)); - bool _isNavigationRailVisible() { final settings = settingsCubit.state; - final viewport = state.cameraViewport; + final viewport = rendererCubit.state.cameraViewport; return settings.navigationRail && settings.navigatorPosition == NavigatorPosition.left && - state.hideUi == HideState.visible && + inputCubit.state.hideUi == HideState.visible && (viewport.width ?? 0) >= LeapBreakpoints.expanded && (viewport.height ?? 0) >= 400; } @@ -2087,7 +1800,7 @@ class CurrentIndexCubit extends Cubit { if (multiplier == null && !positive && currentArea == null) return null; - final viewport = state.cameraViewport; + final viewport = rendererCubit.state.cameraViewport; final transform = customTransform ?? transformCubit.state; final navigationRailOffset = _isNavigationRailVisible() ? kNavigationRailWidth / transform.size @@ -2150,7 +1863,7 @@ class CurrentIndexCubit extends Cubit { return docState.page.areas.firstWhereOrNull((area) { final currentAreaRect = area.rect; - if (exact ?? state.areaNavigatorExact) { + if (exact ?? viewCubit.state.areaNavigatorExact) { return (currentAreaRect.top - rect.top).abs() < precisionErrorTolerance && (currentAreaRect.left - rect.left).abs() < @@ -2206,7 +1919,7 @@ class CurrentIndexCubit extends Cubit { return; } - if (!state.areaNavigatorCreate || createAreaName == null) return; + if (!viewCubit.state.areaNavigatorCreate || createAreaName == null) return; final name = await createAreaName(); if (name == null) return; @@ -2228,7 +1941,7 @@ class CurrentIndexCubit extends Cubit { } void move(Offset delta, {bool force = false, Area? currentArea}) { - final utilitiesState = state.utilities; + final utilitiesState = viewCubit.state.utilities; if (!force) { if (utilitiesState.lockHorizontal) delta = Offset(0, delta.dy); if (utilitiesState.lockVertical) delta = Offset(delta.dx, 0); @@ -2280,7 +1993,7 @@ class CurrentIndexCubit extends Cubit { } void zoom(double delta, [Offset cursor = Offset.zero, bool force = false]) { - final utilitiesState = state.utilities; + final utilitiesState = viewCubit.state.utilities; if (utilitiesState.lockZoom && !force) { delta = 1; } @@ -2300,7 +2013,7 @@ class CurrentIndexCubit extends Cubit { } void size(double size, [Offset cursor = Offset.zero, bool force = false]) { - final utilitiesState = state.utilities; + final utilitiesState = viewCubit.state.utilities; if (utilitiesState.lockZoom && !force) return; if (force) { transformCubit.size(size, cursor); @@ -2320,7 +2033,7 @@ class CurrentIndexCubit extends Cubit { }) { final settings = settingsCubit.state; if (!settings.hasFlag('smoothNavigation')) return; - final utilitiesState = state.utilities; + final utilitiesState = viewCubit.state.utilities; Rect? bounds; var outOfBounds = false; if (!force) { @@ -2376,35 +2089,21 @@ class CurrentIndexCubit extends Cubit { ); } - void toggleKeyboardHideUI() => emit( - state.copyWith( - hideUi: state.hideUi == HideState.visible - ? HideState.keyboard - : HideState.visible, - ), - ); - - void enterTouchHideUI() => emit(state.copyWith(hideUi: HideState.touch)); - - void exitHideUI() => emit(state.copyWith(hideUi: HideState.visible)); - ExternalStorage? getRemoteStorage() => - settingsCubit.getRemote(state.location.remote); - - final _savingLock = Lock(); + settingsCubit.getRemote(saveCubit.state.location.remote); bool hasAutosave() => settingsCubit.state.autosave && (networkingService.isActive || - !(state.embedding?.save ?? true) || + !(saveCubit.state.embedding?.save ?? true) || (!kIsWeb && - !state.absolute && - (state.location.isEmpty || - (state.location.fileType?.isNote() ?? false)) && - (state.location.remote.isEmpty || + !saveCubit.state.absolute && + (saveCubit.state.location.isEmpty || + (saveCubit.state.location.fileType?.isNote() ?? false)) && + (saveCubit.state.location.remote.isEmpty || (settingsCubit - .getRemote(state.location.remote) - ?.hasDocumentCached(state.location.path) ?? + .getRemote(saveCubit.state.location.remote) + ?.hasDocumentCached(saveCubit.state.location.path) ?? false)))); Future save( @@ -2413,53 +2112,50 @@ class CurrentIndexCubit extends Cubit { bool force = false, bool isAutosave = false, }) async { - final absolute = state.absolute; + final absolute = saveCubit.state.absolute; if (location == null && - (state.saved == SaveState.saved || - state.saved == SaveState.absoluteRead)) { - return state.location; + !force && + (saveCubit.state.saved == SaveState.saved || + saveCubit.state.saved == SaveState.absoluteRead)) { + return saveCubit.state.location; } if (networkingService.isClient) { return AssetLocation.empty; } - if (state.isSaveDelayed && isAutosave) { - return state.location; + if (saveCubit.state.isSaveDelayed && isAutosave) { + return saveCubit.state.location; } final storage = getRemoteStorage(); final fileSystem = bloc.state.fileSystem.buildDocumentSystem(storage); final isDelayed = settingsCubit.state.delayedAutosave; if (isDelayed && isAutosave) { final seconds = max(0, settingsCubit.state.autosaveDelaySeconds); - emit(state.copyWith(isSaveDelayed: true)); + saveCubit.setDelayed(true); await Future.delayed(Duration(seconds: seconds)); - if (!state.isSaveDelayed) { - return state.location; + if (!saveCubit.state.isSaveDelayed) { + return saveCubit.state.location; } } - return _savingLock.synchronized(() async { + return saveCubit.savingLock.synchronized(() async { if (location == null && - (state.saved == SaveState.saved || - state.saved == SaveState.absoluteRead)) { - return state.location; + !force && + (saveCubit.state.saved == SaveState.saved || + saveCubit.state.saved == SaveState.absoluteRead)) { + return saveCubit.state.location; } - var current = location ?? state.location; + var current = location ?? saveCubit.state.location; if (isClosed) { return current; } - emit( - state.copyWith( - saved: SaveState.saving, - location: current, - isSaveDelayed: false, - ), - ); + saveCubit.setSaveState(saved: SaveState.saving, location: current); + saveCubit.setDelayed(false); final blocState = bloc.state; - final currentData = await blocState.saveData(null, state.viewOption); + final currentData = await blocState.saveData(); if (isClosed) { return current; } - if (currentData == null || state.embedding != null) { - emit(state.copyWith(saved: SaveState.saved)); + if (currentData == null || saveCubit.state.embedding != null) { + saveCubit.setSaveState(saved: SaveState.saved); return AssetLocation.empty; } if (absolute || !(current.fileType?.isNote() ?? false)) { @@ -2470,8 +2166,8 @@ class CurrentIndexCubit extends Cubit { directory: absolute ? null : current.fileExtension.isEmpty - ? state.location.path - : state.location.parent, + ? saveCubit.state.location.path + : saveCubit.state.location.parent, file, ); current = document.location; @@ -2486,53 +2182,53 @@ class CurrentIndexCubit extends Cubit { if (isClosed) { return current; } - emit( - state.copyWith( - saved: state.saved == SaveState.saving - ? SaveState.saved - : state.saved, - location: current, - ), + saveCubit.setSaveState( + saved: saveCubit.state.saved == SaveState.saving + ? SaveState.saved + : saveCubit.state.saved, + location: current, ); return current; }); } - @override Future close() async { + if (_closed) return; _isClosing = true; - final currentState = state; + _closed = true; final bloc = _activeDocumentBloc; if (bloc != null) { - state.handler.dispose(bloc); - state.temporaryHandler?.dispose(bloc); - for (final handler in state.toggleableHandlers.values) { - handler.dispose(bloc); - } + await toolCubit.disposeRuntime(bloc); } _documentBloc = null; - _disposeAllForegrounds(); - for (final renderer in renderers) { - renderer.dispose(); - } - currentState.cameraViewport.disposeImages(); + await rendererCubit.disposeRuntime(); await _transformSubscription?.cancel(); _transformSubscription = null; + await _rendererSubscription?.cancel(); + _rendererSubscription = null; + await _toolSubscription?.cancel(); + _toolSubscription = null; + await _inputSubscription?.cancel(); + _inputSubscription = null; + await _viewSubscription?.cancel(); + _viewSubscription = null; _transformDebounceTimer?.cancel(); _transformDebounceTimer = null; _networkingDebounceTimer?.cancel(); _networkingDebounceTimer = null; - await _delayedBakeRunner.disposeAndWait(); - await _foregroundRefreshRunner.disposeAndWait(); + await rendererCubit.close(); + await toolCubit.close(); + await inputCubit.close(); + await saveCubit.close(); + await viewCubit.close(); if (!networkingService.isClosed) { await networkingService.close(); } - return super.close(); } Rect getPageRect({Set? invisibleLayers}) { Rect? rect; - for (final renderer in renderers) { + for (final renderer in rendererCubit.renderers) { final rendererRect = renderer.expandedRect; if (rendererRect == null) continue; if (invisibleLayers?.contains(renderer.layer) ?? false) { @@ -2571,7 +2267,7 @@ class CurrentIndexCubit extends Cubit { } final blocState = bloc.state; if (blocState is! DocumentLoadSuccess) return; - state.handler.onDocumentUpdated(blocState, oldState); + toolCubit.state.handler.onDocumentUpdated(blocState, oldState); final addsCombinedHighlight = addedElements.any( (renderer) => @@ -2585,7 +2281,7 @@ class CurrentIndexCubit extends Cubit { } else if (addsCombinedHighlight) { await this.unbake( blocState, - unbakedElements: [...renderers, ...addedElements], + unbakedElements: [...rendererCubit.renderers, ...addedElements], ); } else if (unbake) { await this.unbake(blocState, backgrounds: backgrounds); @@ -2596,7 +2292,7 @@ class CurrentIndexCubit extends Cubit { } setSaveState(saved: SaveState.unsaved); - if (state.embedding != null) { + if (saveCubit.state.embedding != null) { return; } if (reset) { @@ -2620,32 +2316,25 @@ class CurrentIndexCubit extends Cubit { } } - void setAreaNavigatorCreate(bool value) => - emit(state.copyWith(areaNavigatorCreate: value)); - - void setAreaNavigatorExact(bool value) => - emit(state.copyWith(areaNavigatorExact: value)); - - void setAreaNavigatorAsk(bool value) => - emit(state.copyWith(areaNavigatorAsk: value)); - Future updateTogglingTools(DocumentBloc bloc, List tools) async { final blocState = bloc.state; if (blocState is! DocumentLoadSuccess) return; - final newHandlers = Map>.from(state.toggleableHandlers); + final newHandlers = Map>.from( + toolCubit.state.toggleableHandlers, + ); final newForegrounds = Map>.from( - state.toggleableForegrounds, + toolCubit.state.toggleableForegrounds, ); final currentTools = blocState.info.tools; for (final tool in tools) { if (tool.id == null) continue; final index = currentTools.indexWhere((element) => element.id == tool.id); if (index == -1) continue; - final old = state.toggleableHandlers[index]; + final old = toolCubit.state.toggleableHandlers[index]; if (old == null) continue; if (old.data == tool) continue; old.dispose(bloc); - for (final r in state.toggleableForegrounds[index] ?? []) { + for (final r in toolCubit.state.toggleableForegrounds[index] ?? []) { r.dispose(); } final handler = Handler.fromTool(tool); @@ -2675,24 +2364,11 @@ class CurrentIndexCubit extends Cubit { newHandlers[index] = handler; newForegrounds[index] = foregrounds; } - emit( - state.copyWith( - toggleableHandlers: newHandlers, - toggleableForegrounds: newForegrounds, - ), - ); - } - - void setNavigatorEnabled(bool value) { - emit(state.copyWith(navigatorEnabled: value)); - } - - void setNavigatorPage(NavigatorPage page) { - emit(state.copyWith(navigatorPage: page)); + toolCubit.setToggleable(handlers: newHandlers, foregrounds: newForegrounds); } void cancelDelayedBake() { - _delayedBakeRunner.cancel(); + rendererCubit.delayedBakeRunner.cancel(); } Future delayedBake( @@ -2701,9 +2377,9 @@ class CurrentIndexCubit extends Cubit { double? pixelRatio, bool reset = false, bool testTransform = false, - }) => _delayedBakeRunner.schedule(() async { + }) => rendererCubit.delayedBakeRunner.schedule(() async { final newTransform = transformCubit.state; - final viewport = state.cameraViewport; + final viewport = rendererCubit.state.cameraViewport; if (testTransform && newTransform.size == viewport.scale && @@ -2719,28 +2395,26 @@ class CurrentIndexCubit extends Cubit { ); }); - void setUserName(String name) { - emit(state.copyWith(userName: name)); - } - Future reloadTool( DocumentBloc bloc, [ DocumentLoaded? blocState, ]) async { final current = blocState ?? bloc.state; if (current is! DocumentLoaded) return; - // If tool is not the same, change tool final tools = current.info.tools; - final toolIndex = state.index ?? 0; + final toolIndex = toolCubit.state.index ?? 0; final newTool = tools.elementAtOrNull(toolIndex); if (newTool?.isAction() ?? true) { await changeTool(bloc, index: 0, allowBake: false); - } else if (newTool != state.handler.data) { + } else if (newTool != toolCubit.state.handler.data) { await changeTool(bloc, index: toolIndex, allowBake: false); } } - Future reload(DocumentBloc bloc, [DocumentLoaded? blocState]) async { + Future reloadRuntime( + DocumentBloc bloc, [ + DocumentLoaded? blocState, + ]) async { final current = blocState ?? bloc.state; if (current is! DocumentLoaded) return; await reloadTool(bloc, current); @@ -2749,7 +2423,3 @@ class CurrentIndexCubit extends Cubit { await delayedBake(current); } } - -Future _toFile((NoteData, bool) args) async { - return args.$1.toFile(isTextBased: args.$2); -} diff --git a/app/lib/cubits/editor_runtime.dart b/app/lib/cubits/editor_runtime.dart new file mode 100644 index 000000000000..bd5857f5d13c --- /dev/null +++ b/app/lib/cubits/editor_runtime.dart @@ -0,0 +1,551 @@ +import 'package:butterfly/embed/embedding.dart'; +import 'package:butterfly/handlers/handler.dart'; +import 'package:butterfly/helpers/async.dart'; +import 'package:butterfly/models/viewport.dart'; +import 'package:butterfly/renderers/renderer.dart'; +import 'package:butterfly/selections/selection.dart'; +import 'package:butterfly/cubits/settings.dart'; +import 'package:butterfly/cubits/editor_session.dart'; +import 'package:butterfly/views/navigator/view.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lw_file_system/lw_file_system.dart'; +import 'package:synchronized/synchronized.dart'; + +part 'editor_runtime.freezed.dart'; + +enum SaveState { saved, saving, unsaved, absoluteRead } + +enum HideState { visible, keyboard, touch } + +enum RendererState { visible, temporary, hidden } + +enum TemporaryState { allowClick, removeAfterClick, removeAfterRelease } + +@Freezed(equal: false) +sealed class RendererRuntimeState with _$RendererRuntimeState { + const RendererRuntimeState._(); + + const factory RendererRuntimeState({ + @Default(CameraViewport.unbaked()) CameraViewport cameraViewport, + @Default({}) Map rendererStates, + @Default({}) Map? temporaryRendererStates, + }) = _RendererRuntimeState; + + Map get allRendererStates => { + ...rendererStates, + ...?temporaryRendererStates, + }; +} + +class RendererCubit extends Cubit { + RendererCubit( + this.settingsCubit, [ + super.initial = const RendererRuntimeState(), + ]); + + final SettingsCubit settingsCubit; + + final initializedElements = >{}; + final bakeLock = Lock(); + final delayedBakeRunner = CoalescedAsyncRunner( + delay: const Duration(milliseconds: 100), + ); + + void replace(RendererRuntimeState state) => emit(state); + + void setViewport(CameraViewport cameraViewport) => + emit(state.copyWith(cameraViewport: cameraViewport)); + + void setRendererStates({ + Map? rendererStates, + Map? temporaryRendererStates, + }) => emit( + state.copyWith( + rendererStates: rendererStates ?? state.rendererStates, + temporaryRendererStates: + temporaryRendererStates ?? state.temporaryRendererStates, + ), + ); + + List> get renderers => + List>.from(state.cameraViewport.bakedElements) + ..addAll(state.cameraViewport.unbakedElements); + + Renderer? getRenderer(PadElement element) => + renderers.firstWhereOrNull((renderer) => renderer.element == element); + + Future disposeRuntime() async { + delayedBakeRunner.cancel(); + await delayedBakeRunner.disposeAndWait(); + initializedElements.clear(); + state.cameraViewport.disposeImages(); + for (final renderer in renderers) { + renderer.dispose(); + } + } +} + +@Freezed(equal: false) +sealed class ToolRuntimeState with _$ToolRuntimeState { + const ToolRuntimeState._(); + + const factory ToolRuntimeState({ + int? index, + required Handler handler, + Handler? temporaryHandler, + int? temporaryIndex, + @Default([]) List foregrounds, + Selection? selection, + @Default(false) bool pinned, + List? temporaryForegrounds, + @Default({}) Map> toggleableHandlers, + @Default([]) List networkingForegrounds, + @Default({}) Map> toggleableForegrounds, + @Default(MouseCursor.defer) MouseCursor cursor, + MouseCursor? temporaryCursor, + @Default(TemporaryState.allowClick) TemporaryState temporaryState, + PreferredSizeWidget? toolbar, + PreferredSizeWidget? temporaryToolbar, + }) = _ToolRuntimeState; + + MouseCursor get currentCursor => temporaryCursor ?? cursor; + + List getAllForegrounds([bool networking = true]) => [ + ...foregrounds, + ...?temporaryForegrounds, + ...toggleableForegrounds.values.expand((e) => e), + if (networking) ...networkingForegrounds, + ]; +} + +class ToolCubit extends Cubit { + ToolCubit([ToolRuntimeState? initial]) + : super(initial ?? ToolRuntimeState(handler: HandHandler())); + + final foregroundRefreshRunner = CoalescedAsyncRunner(delay: Duration.zero); + + void replace(ToolRuntimeState state) => emit(state); + + Handler getHandler({bool disableTemporary = false, bool editable = true}) { + if (!editable) return HandHandler(); + return disableTemporary + ? state.handler + : state.temporaryHandler ?? state.handler; + } + + T? fetchHandler({ + bool disableTemporary = false, + bool editable = true, + }) { + final handler = getHandler( + disableTemporary: disableTemporary, + editable: editable, + ); + if (handler is T) return handler; + return null; + } + + void setActiveTool({ + required int? index, + required Handler handler, + required MouseCursor cursor, + required List foregrounds, + required PreferredSizeWidget? toolbar, + required Map rendererStates, + }) => emit( + state.copyWith( + index: index, + handler: handler, + cursor: cursor, + foregrounds: foregrounds, + toolbar: toolbar, + temporaryForegrounds: null, + temporaryHandler: null, + temporaryToolbar: null, + temporaryCursor: null, + temporaryIndex: null, + ), + ); + + void setTemporaryTool({ + required Handler? handler, + required int? index, + required List? foregrounds, + required PreferredSizeWidget? toolbar, + required MouseCursor? cursor, + required Map? rendererStates, + TemporaryState? temporaryState, + }) => emit( + state.copyWith( + temporaryHandler: handler, + temporaryIndex: index, + temporaryForegrounds: foregrounds, + temporaryToolbar: toolbar, + temporaryCursor: cursor, + temporaryState: temporaryState ?? state.temporaryState, + ), + ); + + void setToggleable({ + Map>? handlers, + Map>? foregrounds, + }) => emit( + state.copyWith( + toggleableHandlers: handlers ?? state.toggleableHandlers, + toggleableForegrounds: foregrounds ?? state.toggleableForegrounds, + ), + ); + + void setForegrounds({ + List? foregrounds, + List? temporaryForegrounds, + Map>? toggleableForegrounds, + List? networkingForegrounds, + MouseCursor? cursor, + MouseCursor? temporaryCursor, + Map? rendererStates, + Map? temporaryRendererStates, + }) => emit( + state.copyWith( + foregrounds: foregrounds ?? state.foregrounds, + temporaryForegrounds: temporaryForegrounds ?? state.temporaryForegrounds, + toggleableForegrounds: + toggleableForegrounds ?? state.toggleableForegrounds, + networkingForegrounds: + networkingForegrounds ?? state.networkingForegrounds, + cursor: cursor ?? state.cursor, + temporaryCursor: temporaryCursor ?? state.temporaryCursor, + ), + ); + + void setToolbar({ + PreferredSizeWidget? toolbar, + PreferredSizeWidget? temporaryToolbar, + }) => emit( + state.copyWith( + toolbar: toolbar ?? state.toolbar, + temporaryToolbar: temporaryToolbar ?? state.temporaryToolbar, + ), + ); + + void setCursor(MouseCursor cursor) { + if (state.cursor != cursor) emit(state.copyWith(cursor: cursor)); + } + + void setIndex(int? index) => emit(state.copyWith(index: index)); + + void setSelection(Selection? selection) => + emit(state.copyWith(selection: selection)); + + void insertSelection(dynamic selected, [bool toggle = true]) { + final selection = state.selection; + if (selection == null) { + setSelection(Selection.from(selected)); + return; + } + Selection? next; + if (selection.selected.contains(selected) && toggle) { + if (selection.selected.length != 1) { + next = selection.remove(selected); + } + } else { + next = selection.insert(selected); + } + setSelection(next); + } + + void changeSelection(dynamic selected, [bool toggle = true]) { + Selection? selection; + if (selected is Selection?) { + selection = selected; + } else if (!toggle || !(state.selection?.selected.contains(selected) ?? false)) { + selection = Selection.from(selected); + } + setSelection(selection); + } + + void removeSelection(List selected) { + Selection? selection = state.selection; + if (selection == null) return; + for (final s in selected) { + selection = selection?.remove(s); + } + setSelection(selection); + } + + void resetSelection({bool force = false}) { + if (force || !state.pinned) emit(state.copyWith(selection: null)); + } + + Tool? getTool(DocumentInfo info) { + final index = state.index; + if (index == null || info.tools.isEmpty || index < 0 || index >= info.tools.length) { + return null; + } + return info.tools[index]; + } + + T? fetchTool(DocumentInfo info) { + final tool = getTool(info); + if (tool is T) return tool; + return null; + } + + void togglePin() => emit(state.copyWith(pinned: !state.pinned)); + + void setTemporaryState(TemporaryState temporaryState) => + emit(state.copyWith(temporaryState: temporaryState)); + + void resetRuntime() => emit(ToolRuntimeState(handler: HandHandler())); + + Future resetInput(dynamic bloc, EditorInputCubit inputCubit) async { + await state.handler.resetInput(bloc); + inputCubit.resetInputState(); + } + + void changeTemporaryHandlerMove(RendererCubit rendererCubit) { + setTemporaryTool( + handler: HandHandler(), + index: null, + foregrounds: null, + toolbar: null, + cursor: null, + rendererStates: null, + ); + rendererCubit.setRendererStates(temporaryRendererStates: const {}); + } + + Future disposeRuntime(dynamic bloc) async { + state.handler.dispose(bloc); + state.temporaryHandler?.dispose(bloc); + for (final handler in state.toggleableHandlers.values) { + handler.dispose(bloc); + } + for (final renderer in state.getAllForegrounds()) { + renderer.dispose(); + } + foregroundRefreshRunner.cancel(); + await foregroundRefreshRunner.disposeAndWait(); + } +} + +@freezed +sealed class EditorInputState with _$EditorInputState { + const factory EditorInputState({ + Offset? lastPosition, + @Default([]) List pointers, + int? buttons, + @Default(false) bool penDetected, + @Default(false) bool sessionPenOnlyInput, + @Default(HideState.visible) HideState hideUi, + }) = _EditorInputState; +} + +class EditorInputCubit extends Cubit { + EditorInputCubit( + this.settingsCubit, [ + super.initial = const EditorInputState(), + ]); + + final SettingsCubit settingsCubit; + + void replace(EditorInputState state) => emit(state); + + void setPenDetected(bool detected, {bool enableSessionPenOnly = false}) { + if (state.penDetected == detected && + (!enableSessionPenOnly || state.sessionPenOnlyInput)) { + return; + } + emit( + state.copyWith( + penDetected: detected, + sessionPenOnlyInput: enableSessionPenOnly + ? true + : state.sessionPenOnlyInput, + ), + ); + } + + bool get effectivePenOnlyInput { + final setting = settingsCubit.state.penOnlyInput; + if (setting != null) return setting; + return state.sessionPenOnlyInput; + } + + bool get moveEnabled => + (settingsCubit.state.inputGestures && state.pointers.length > 1) && + settingsCubit.state.moveOnGesture; + + void detectPen(bool detected) { + if (state.penDetected == detected) return; + setPenDetected( + detected, + enableSessionPenOnly: + detected && + settingsCubit.state.penOnlyInput == null && + !state.sessionPenOnlyInput, + ); + } + + void setSessionPenOnlyInput(bool value) { + if (state.sessionPenOnlyInput != value) { + emit(state.copyWith(sessionPenOnlyInput: value)); + } + } + + void updateLastPosition(Offset position) { + final lastPos = state.lastPosition; + if (lastPos != null) { + final dx = (position.dx - lastPos.dx).abs(); + final dy = (position.dy - lastPos.dy).abs(); + if (dx < 1 && dy < 1) return; + } + emit(state.copyWith(lastPosition: position)); + } + + void addPointer(int pointer) { + if (!state.pointers.contains(pointer)) { + emit(state.copyWith(pointers: [...state.pointers, pointer])); + } + } + + void removePointer(int pointer) { + if (state.pointers.contains(pointer)) { + emit( + state.copyWith( + pointers: state.pointers.where((p) => p != pointer).toList(), + ), + ); + } + } + + void setButtons(int buttons) => emit(state.copyWith(buttons: buttons)); + + void removeButtons() => emit(state.copyWith(buttons: null)); + + void resetInputState() => emit(state.copyWith(buttons: null, pointers: [])); + + void toggleKeyboardHideUI() => emit( + state.copyWith( + hideUi: state.hideUi == HideState.visible + ? HideState.keyboard + : HideState.visible, + ), + ); + + void enterTouchHideUI() => emit(state.copyWith(hideUi: HideState.touch)); + + void exitHideUI() => emit(state.copyWith(hideUi: HideState.visible)); +} + +@freezed +sealed class DocumentSaveState with _$DocumentSaveState { + const DocumentSaveState._(); + + const factory DocumentSaveState({ + @Default(false) bool isSaveDelayed, + @Default(AssetLocation(path: '')) AssetLocation location, + Embedding? embedding, + @Default(SaveState.saved) SaveState saved, + @Default(false) bool isCreating, + }) = _DocumentSaveState; + + bool get absolute => saved == SaveState.absoluteRead; +} + +class DocumentSaveCubit extends Cubit { + DocumentSaveCubit( + this.settingsCubit, [ + super.initial = const DocumentSaveState(), + ]); + + final SettingsCubit settingsCubit; + + final savingLock = Lock(); + + void replace(DocumentSaveState state) => emit(state); + + void setSaveState({ + AssetLocation? location, + SaveState? saved, + bool absolute = false, + bool? isCreating, + bool keepRead = false, + }) => emit( + state.copyWith( + location: location ?? state.location, + isCreating: isCreating ?? state.isCreating, + saved: (absolute || (keepRead && state.absolute)) + ? SaveState.absoluteRead + : saved ?? state.saved, + ), + ); + + void setDelayed(bool delayed) => emit(state.copyWith(isSaveDelayed: delayed)); +} + +@freezed +sealed class EditorViewState with _$EditorViewState { + const factory EditorViewState({ + @Default(UtilitiesState()) UtilitiesState utilities, + @Default(ViewOption()) ViewOption viewOption, + @Default(true) bool areaNavigatorCreate, + @Default(true) bool areaNavigatorExact, + @Default(false) bool areaNavigatorAsk, + @Default(false) bool navigatorEnabled, + @Default(NavigatorPage.waypoints) NavigatorPage navigatorPage, + @Default('') String userName, + }) = _EditorViewState; +} + +class EditorViewCubit extends Cubit { + EditorViewCubit({this.editorSessionCubit, EditorViewState? initial}) + : super(initial ?? const EditorViewState()); + + final EditorSessionCubit? editorSessionCubit; + + void replace(EditorViewState state) => emit(state); + + void updateUtilities({UtilitiesState? utilities, ViewOption? view}) { + emit( + state.copyWith( + utilities: utilities ?? state.utilities, + viewOption: view ?? state.viewOption, + ), + ); + if (utilities != null) { + editorSessionCubit?.updateUtilities(utilities); + } + } + + void setAreaNavigator({bool? create, bool? exact, bool? ask}) { + emit( + state.copyWith( + areaNavigatorCreate: create ?? state.areaNavigatorCreate, + areaNavigatorExact: exact ?? state.areaNavigatorExact, + areaNavigatorAsk: ask ?? state.areaNavigatorAsk, + ), + ); + editorSessionCubit?.updateAreaNavigator( + create: create, + exact: exact, + ask: ask, + ); + } + + void setNavigator({bool? enabled, NavigatorPage? page}) { + emit( + state.copyWith( + navigatorEnabled: enabled ?? state.navigatorEnabled, + navigatorPage: page ?? state.navigatorPage, + ), + ); + editorSessionCubit?.updateNavigator(enabled: enabled, page: page); + } + + void setUserName(String name) => emit(state.copyWith(userName: name)); +} diff --git a/app/lib/cubits/editor_runtime.freezed.dart b/app/lib/cubits/editor_runtime.freezed.dart new file mode 100644 index 000000000000..8a984670b232 --- /dev/null +++ b/app/lib/cubits/editor_runtime.freezed.dart @@ -0,0 +1,831 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'editor_runtime.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$RendererRuntimeState { + + CameraViewport get cameraViewport; Map get rendererStates; Map? get temporaryRendererStates; +/// Create a copy of RendererRuntimeState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$RendererRuntimeStateCopyWith get copyWith => _$RendererRuntimeStateCopyWithImpl(this as RendererRuntimeState, _$identity); + + + + + +@override +String toString() { + return 'RendererRuntimeState(cameraViewport: $cameraViewport, rendererStates: $rendererStates, temporaryRendererStates: $temporaryRendererStates)'; +} + + +} + +/// @nodoc +abstract mixin class $RendererRuntimeStateCopyWith<$Res> { + factory $RendererRuntimeStateCopyWith(RendererRuntimeState value, $Res Function(RendererRuntimeState) _then) = _$RendererRuntimeStateCopyWithImpl; +@useResult +$Res call({ + CameraViewport cameraViewport, Map rendererStates, Map? temporaryRendererStates +}); + + +$CameraViewportCopyWith<$Res> get cameraViewport; + +} +/// @nodoc +class _$RendererRuntimeStateCopyWithImpl<$Res> + implements $RendererRuntimeStateCopyWith<$Res> { + _$RendererRuntimeStateCopyWithImpl(this._self, this._then); + + final RendererRuntimeState _self; + final $Res Function(RendererRuntimeState) _then; + +/// Create a copy of RendererRuntimeState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? cameraViewport = null,Object? rendererStates = null,Object? temporaryRendererStates = freezed,}) { + return _then(_self.copyWith( +cameraViewport: null == cameraViewport ? _self.cameraViewport : cameraViewport // ignore: cast_nullable_to_non_nullable +as CameraViewport,rendererStates: null == rendererStates ? _self.rendererStates : rendererStates // ignore: cast_nullable_to_non_nullable +as Map,temporaryRendererStates: freezed == temporaryRendererStates ? _self.temporaryRendererStates : temporaryRendererStates // ignore: cast_nullable_to_non_nullable +as Map?, + )); +} +/// Create a copy of RendererRuntimeState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$CameraViewportCopyWith<$Res> get cameraViewport { + + return $CameraViewportCopyWith<$Res>(_self.cameraViewport, (value) { + return _then(_self.copyWith(cameraViewport: value)); + }); +} +} + + + +/// @nodoc + + +class _RendererRuntimeState extends RendererRuntimeState { + const _RendererRuntimeState({this.cameraViewport = const CameraViewport.unbaked(), final Map rendererStates = const {}, final Map? temporaryRendererStates = const {}}): _rendererStates = rendererStates,_temporaryRendererStates = temporaryRendererStates,super._(); + + +@override@JsonKey() final CameraViewport cameraViewport; + final Map _rendererStates; +@override@JsonKey() Map get rendererStates { + if (_rendererStates is EqualUnmodifiableMapView) return _rendererStates; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_rendererStates); +} + + final Map? _temporaryRendererStates; +@override@JsonKey() Map? get temporaryRendererStates { + final value = _temporaryRendererStates; + if (value == null) return null; + if (_temporaryRendererStates is EqualUnmodifiableMapView) return _temporaryRendererStates; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); +} + + +/// Create a copy of RendererRuntimeState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$RendererRuntimeStateCopyWith<_RendererRuntimeState> get copyWith => __$RendererRuntimeStateCopyWithImpl<_RendererRuntimeState>(this, _$identity); + + + + + +@override +String toString() { + return 'RendererRuntimeState(cameraViewport: $cameraViewport, rendererStates: $rendererStates, temporaryRendererStates: $temporaryRendererStates)'; +} + + +} + +/// @nodoc +abstract mixin class _$RendererRuntimeStateCopyWith<$Res> implements $RendererRuntimeStateCopyWith<$Res> { + factory _$RendererRuntimeStateCopyWith(_RendererRuntimeState value, $Res Function(_RendererRuntimeState) _then) = __$RendererRuntimeStateCopyWithImpl; +@override @useResult +$Res call({ + CameraViewport cameraViewport, Map rendererStates, Map? temporaryRendererStates +}); + + +@override $CameraViewportCopyWith<$Res> get cameraViewport; + +} +/// @nodoc +class __$RendererRuntimeStateCopyWithImpl<$Res> + implements _$RendererRuntimeStateCopyWith<$Res> { + __$RendererRuntimeStateCopyWithImpl(this._self, this._then); + + final _RendererRuntimeState _self; + final $Res Function(_RendererRuntimeState) _then; + +/// Create a copy of RendererRuntimeState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? cameraViewport = null,Object? rendererStates = null,Object? temporaryRendererStates = freezed,}) { + return _then(_RendererRuntimeState( +cameraViewport: null == cameraViewport ? _self.cameraViewport : cameraViewport // ignore: cast_nullable_to_non_nullable +as CameraViewport,rendererStates: null == rendererStates ? _self._rendererStates : rendererStates // ignore: cast_nullable_to_non_nullable +as Map,temporaryRendererStates: freezed == temporaryRendererStates ? _self._temporaryRendererStates : temporaryRendererStates // ignore: cast_nullable_to_non_nullable +as Map?, + )); +} + +/// Create a copy of RendererRuntimeState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$CameraViewportCopyWith<$Res> get cameraViewport { + + return $CameraViewportCopyWith<$Res>(_self.cameraViewport, (value) { + return _then(_self.copyWith(cameraViewport: value)); + }); +} +} + +/// @nodoc +mixin _$ToolRuntimeState { + + int? get index; Handler get handler; Handler? get temporaryHandler; int? get temporaryIndex; List get foregrounds; Selection? get selection; bool get pinned; List? get temporaryForegrounds; Map> get toggleableHandlers; List get networkingForegrounds; Map> get toggleableForegrounds; MouseCursor get cursor; MouseCursor? get temporaryCursor; TemporaryState get temporaryState; PreferredSizeWidget? get toolbar; PreferredSizeWidget? get temporaryToolbar; +/// Create a copy of ToolRuntimeState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ToolRuntimeStateCopyWith get copyWith => _$ToolRuntimeStateCopyWithImpl(this as ToolRuntimeState, _$identity); + + + + + +@override +String toString() { + return 'ToolRuntimeState(index: $index, handler: $handler, temporaryHandler: $temporaryHandler, temporaryIndex: $temporaryIndex, foregrounds: $foregrounds, selection: $selection, pinned: $pinned, temporaryForegrounds: $temporaryForegrounds, toggleableHandlers: $toggleableHandlers, networkingForegrounds: $networkingForegrounds, toggleableForegrounds: $toggleableForegrounds, cursor: $cursor, temporaryCursor: $temporaryCursor, temporaryState: $temporaryState, toolbar: $toolbar, temporaryToolbar: $temporaryToolbar)'; +} + + +} + +/// @nodoc +abstract mixin class $ToolRuntimeStateCopyWith<$Res> { + factory $ToolRuntimeStateCopyWith(ToolRuntimeState value, $Res Function(ToolRuntimeState) _then) = _$ToolRuntimeStateCopyWithImpl; +@useResult +$Res call({ + int? index, Handler handler, Handler? temporaryHandler, int? temporaryIndex, List foregrounds, Selection? selection, bool pinned, List? temporaryForegrounds, Map> toggleableHandlers, List networkingForegrounds, Map> toggleableForegrounds, MouseCursor cursor, MouseCursor? temporaryCursor, TemporaryState temporaryState, PreferredSizeWidget? toolbar, PreferredSizeWidget? temporaryToolbar +}); + + + + +} +/// @nodoc +class _$ToolRuntimeStateCopyWithImpl<$Res> + implements $ToolRuntimeStateCopyWith<$Res> { + _$ToolRuntimeStateCopyWithImpl(this._self, this._then); + + final ToolRuntimeState _self; + final $Res Function(ToolRuntimeState) _then; + +/// Create a copy of ToolRuntimeState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? index = freezed,Object? handler = null,Object? temporaryHandler = freezed,Object? temporaryIndex = freezed,Object? foregrounds = null,Object? selection = freezed,Object? pinned = null,Object? temporaryForegrounds = freezed,Object? toggleableHandlers = null,Object? networkingForegrounds = null,Object? toggleableForegrounds = null,Object? cursor = null,Object? temporaryCursor = freezed,Object? temporaryState = null,Object? toolbar = freezed,Object? temporaryToolbar = freezed,}) { + return _then(_self.copyWith( +index: freezed == index ? _self.index : index // ignore: cast_nullable_to_non_nullable +as int?,handler: null == handler ? _self.handler : handler // ignore: cast_nullable_to_non_nullable +as Handler,temporaryHandler: freezed == temporaryHandler ? _self.temporaryHandler : temporaryHandler // ignore: cast_nullable_to_non_nullable +as Handler?,temporaryIndex: freezed == temporaryIndex ? _self.temporaryIndex : temporaryIndex // ignore: cast_nullable_to_non_nullable +as int?,foregrounds: null == foregrounds ? _self.foregrounds : foregrounds // ignore: cast_nullable_to_non_nullable +as List,selection: freezed == selection ? _self.selection : selection // ignore: cast_nullable_to_non_nullable +as Selection?,pinned: null == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable +as bool,temporaryForegrounds: freezed == temporaryForegrounds ? _self.temporaryForegrounds : temporaryForegrounds // ignore: cast_nullable_to_non_nullable +as List?,toggleableHandlers: null == toggleableHandlers ? _self.toggleableHandlers : toggleableHandlers // ignore: cast_nullable_to_non_nullable +as Map>,networkingForegrounds: null == networkingForegrounds ? _self.networkingForegrounds : networkingForegrounds // ignore: cast_nullable_to_non_nullable +as List,toggleableForegrounds: null == toggleableForegrounds ? _self.toggleableForegrounds : toggleableForegrounds // ignore: cast_nullable_to_non_nullable +as Map>,cursor: null == cursor ? _self.cursor : cursor // ignore: cast_nullable_to_non_nullable +as MouseCursor,temporaryCursor: freezed == temporaryCursor ? _self.temporaryCursor : temporaryCursor // ignore: cast_nullable_to_non_nullable +as MouseCursor?,temporaryState: null == temporaryState ? _self.temporaryState : temporaryState // ignore: cast_nullable_to_non_nullable +as TemporaryState,toolbar: freezed == toolbar ? _self.toolbar : toolbar // ignore: cast_nullable_to_non_nullable +as PreferredSizeWidget?,temporaryToolbar: freezed == temporaryToolbar ? _self.temporaryToolbar : temporaryToolbar // ignore: cast_nullable_to_non_nullable +as PreferredSizeWidget?, + )); +} + +} + + + +/// @nodoc + + +class _ToolRuntimeState extends ToolRuntimeState { + const _ToolRuntimeState({this.index, required this.handler, this.temporaryHandler, this.temporaryIndex, final List foregrounds = const [], this.selection, this.pinned = false, final List? temporaryForegrounds, final Map> toggleableHandlers = const {}, final List networkingForegrounds = const [], final Map> toggleableForegrounds = const {}, this.cursor = MouseCursor.defer, this.temporaryCursor, this.temporaryState = TemporaryState.allowClick, this.toolbar, this.temporaryToolbar}): _foregrounds = foregrounds,_temporaryForegrounds = temporaryForegrounds,_toggleableHandlers = toggleableHandlers,_networkingForegrounds = networkingForegrounds,_toggleableForegrounds = toggleableForegrounds,super._(); + + +@override final int? index; +@override final Handler handler; +@override final Handler? temporaryHandler; +@override final int? temporaryIndex; + final List _foregrounds; +@override@JsonKey() List get foregrounds { + if (_foregrounds is EqualUnmodifiableListView) return _foregrounds; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_foregrounds); +} + +@override final Selection? selection; +@override@JsonKey() final bool pinned; + final List? _temporaryForegrounds; +@override List? get temporaryForegrounds { + final value = _temporaryForegrounds; + if (value == null) return null; + if (_temporaryForegrounds is EqualUnmodifiableListView) return _temporaryForegrounds; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + + final Map> _toggleableHandlers; +@override@JsonKey() Map> get toggleableHandlers { + if (_toggleableHandlers is EqualUnmodifiableMapView) return _toggleableHandlers; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_toggleableHandlers); +} + + final List _networkingForegrounds; +@override@JsonKey() List get networkingForegrounds { + if (_networkingForegrounds is EqualUnmodifiableListView) return _networkingForegrounds; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_networkingForegrounds); +} + + final Map> _toggleableForegrounds; +@override@JsonKey() Map> get toggleableForegrounds { + if (_toggleableForegrounds is EqualUnmodifiableMapView) return _toggleableForegrounds; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_toggleableForegrounds); +} + +@override@JsonKey() final MouseCursor cursor; +@override final MouseCursor? temporaryCursor; +@override@JsonKey() final TemporaryState temporaryState; +@override final PreferredSizeWidget? toolbar; +@override final PreferredSizeWidget? temporaryToolbar; + +/// Create a copy of ToolRuntimeState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ToolRuntimeStateCopyWith<_ToolRuntimeState> get copyWith => __$ToolRuntimeStateCopyWithImpl<_ToolRuntimeState>(this, _$identity); + + + + + +@override +String toString() { + return 'ToolRuntimeState(index: $index, handler: $handler, temporaryHandler: $temporaryHandler, temporaryIndex: $temporaryIndex, foregrounds: $foregrounds, selection: $selection, pinned: $pinned, temporaryForegrounds: $temporaryForegrounds, toggleableHandlers: $toggleableHandlers, networkingForegrounds: $networkingForegrounds, toggleableForegrounds: $toggleableForegrounds, cursor: $cursor, temporaryCursor: $temporaryCursor, temporaryState: $temporaryState, toolbar: $toolbar, temporaryToolbar: $temporaryToolbar)'; +} + + +} + +/// @nodoc +abstract mixin class _$ToolRuntimeStateCopyWith<$Res> implements $ToolRuntimeStateCopyWith<$Res> { + factory _$ToolRuntimeStateCopyWith(_ToolRuntimeState value, $Res Function(_ToolRuntimeState) _then) = __$ToolRuntimeStateCopyWithImpl; +@override @useResult +$Res call({ + int? index, Handler handler, Handler? temporaryHandler, int? temporaryIndex, List foregrounds, Selection? selection, bool pinned, List? temporaryForegrounds, Map> toggleableHandlers, List networkingForegrounds, Map> toggleableForegrounds, MouseCursor cursor, MouseCursor? temporaryCursor, TemporaryState temporaryState, PreferredSizeWidget? toolbar, PreferredSizeWidget? temporaryToolbar +}); + + + + +} +/// @nodoc +class __$ToolRuntimeStateCopyWithImpl<$Res> + implements _$ToolRuntimeStateCopyWith<$Res> { + __$ToolRuntimeStateCopyWithImpl(this._self, this._then); + + final _ToolRuntimeState _self; + final $Res Function(_ToolRuntimeState) _then; + +/// Create a copy of ToolRuntimeState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? index = freezed,Object? handler = null,Object? temporaryHandler = freezed,Object? temporaryIndex = freezed,Object? foregrounds = null,Object? selection = freezed,Object? pinned = null,Object? temporaryForegrounds = freezed,Object? toggleableHandlers = null,Object? networkingForegrounds = null,Object? toggleableForegrounds = null,Object? cursor = null,Object? temporaryCursor = freezed,Object? temporaryState = null,Object? toolbar = freezed,Object? temporaryToolbar = freezed,}) { + return _then(_ToolRuntimeState( +index: freezed == index ? _self.index : index // ignore: cast_nullable_to_non_nullable +as int?,handler: null == handler ? _self.handler : handler // ignore: cast_nullable_to_non_nullable +as Handler,temporaryHandler: freezed == temporaryHandler ? _self.temporaryHandler : temporaryHandler // ignore: cast_nullable_to_non_nullable +as Handler?,temporaryIndex: freezed == temporaryIndex ? _self.temporaryIndex : temporaryIndex // ignore: cast_nullable_to_non_nullable +as int?,foregrounds: null == foregrounds ? _self._foregrounds : foregrounds // ignore: cast_nullable_to_non_nullable +as List,selection: freezed == selection ? _self.selection : selection // ignore: cast_nullable_to_non_nullable +as Selection?,pinned: null == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable +as bool,temporaryForegrounds: freezed == temporaryForegrounds ? _self._temporaryForegrounds : temporaryForegrounds // ignore: cast_nullable_to_non_nullable +as List?,toggleableHandlers: null == toggleableHandlers ? _self._toggleableHandlers : toggleableHandlers // ignore: cast_nullable_to_non_nullable +as Map>,networkingForegrounds: null == networkingForegrounds ? _self._networkingForegrounds : networkingForegrounds // ignore: cast_nullable_to_non_nullable +as List,toggleableForegrounds: null == toggleableForegrounds ? _self._toggleableForegrounds : toggleableForegrounds // ignore: cast_nullable_to_non_nullable +as Map>,cursor: null == cursor ? _self.cursor : cursor // ignore: cast_nullable_to_non_nullable +as MouseCursor,temporaryCursor: freezed == temporaryCursor ? _self.temporaryCursor : temporaryCursor // ignore: cast_nullable_to_non_nullable +as MouseCursor?,temporaryState: null == temporaryState ? _self.temporaryState : temporaryState // ignore: cast_nullable_to_non_nullable +as TemporaryState,toolbar: freezed == toolbar ? _self.toolbar : toolbar // ignore: cast_nullable_to_non_nullable +as PreferredSizeWidget?,temporaryToolbar: freezed == temporaryToolbar ? _self.temporaryToolbar : temporaryToolbar // ignore: cast_nullable_to_non_nullable +as PreferredSizeWidget?, + )); +} + + +} + +/// @nodoc +mixin _$EditorInputState { + + Offset? get lastPosition; List get pointers; int? get buttons; bool get penDetected; bool get sessionPenOnlyInput; HideState get hideUi; +/// Create a copy of EditorInputState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$EditorInputStateCopyWith get copyWith => _$EditorInputStateCopyWithImpl(this as EditorInputState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is EditorInputState&&(identical(other.lastPosition, lastPosition) || other.lastPosition == lastPosition)&&const DeepCollectionEquality().equals(other.pointers, pointers)&&(identical(other.buttons, buttons) || other.buttons == buttons)&&(identical(other.penDetected, penDetected) || other.penDetected == penDetected)&&(identical(other.sessionPenOnlyInput, sessionPenOnlyInput) || other.sessionPenOnlyInput == sessionPenOnlyInput)&&(identical(other.hideUi, hideUi) || other.hideUi == hideUi)); +} + + +@override +int get hashCode => Object.hash(runtimeType,lastPosition,const DeepCollectionEquality().hash(pointers),buttons,penDetected,sessionPenOnlyInput,hideUi); + +@override +String toString() { + return 'EditorInputState(lastPosition: $lastPosition, pointers: $pointers, buttons: $buttons, penDetected: $penDetected, sessionPenOnlyInput: $sessionPenOnlyInput, hideUi: $hideUi)'; +} + + +} + +/// @nodoc +abstract mixin class $EditorInputStateCopyWith<$Res> { + factory $EditorInputStateCopyWith(EditorInputState value, $Res Function(EditorInputState) _then) = _$EditorInputStateCopyWithImpl; +@useResult +$Res call({ + Offset? lastPosition, List pointers, int? buttons, bool penDetected, bool sessionPenOnlyInput, HideState hideUi +}); + + + + +} +/// @nodoc +class _$EditorInputStateCopyWithImpl<$Res> + implements $EditorInputStateCopyWith<$Res> { + _$EditorInputStateCopyWithImpl(this._self, this._then); + + final EditorInputState _self; + final $Res Function(EditorInputState) _then; + +/// Create a copy of EditorInputState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? lastPosition = freezed,Object? pointers = null,Object? buttons = freezed,Object? penDetected = null,Object? sessionPenOnlyInput = null,Object? hideUi = null,}) { + return _then(_self.copyWith( +lastPosition: freezed == lastPosition ? _self.lastPosition : lastPosition // ignore: cast_nullable_to_non_nullable +as Offset?,pointers: null == pointers ? _self.pointers : pointers // ignore: cast_nullable_to_non_nullable +as List,buttons: freezed == buttons ? _self.buttons : buttons // ignore: cast_nullable_to_non_nullable +as int?,penDetected: null == penDetected ? _self.penDetected : penDetected // ignore: cast_nullable_to_non_nullable +as bool,sessionPenOnlyInput: null == sessionPenOnlyInput ? _self.sessionPenOnlyInput : sessionPenOnlyInput // ignore: cast_nullable_to_non_nullable +as bool,hideUi: null == hideUi ? _self.hideUi : hideUi // ignore: cast_nullable_to_non_nullable +as HideState, + )); +} + +} + + + +/// @nodoc + + +class _EditorInputState implements EditorInputState { + const _EditorInputState({this.lastPosition, final List pointers = const [], this.buttons, this.penDetected = false, this.sessionPenOnlyInput = false, this.hideUi = HideState.visible}): _pointers = pointers; + + +@override final Offset? lastPosition; + final List _pointers; +@override@JsonKey() List get pointers { + if (_pointers is EqualUnmodifiableListView) return _pointers; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_pointers); +} + +@override final int? buttons; +@override@JsonKey() final bool penDetected; +@override@JsonKey() final bool sessionPenOnlyInput; +@override@JsonKey() final HideState hideUi; + +/// Create a copy of EditorInputState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$EditorInputStateCopyWith<_EditorInputState> get copyWith => __$EditorInputStateCopyWithImpl<_EditorInputState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _EditorInputState&&(identical(other.lastPosition, lastPosition) || other.lastPosition == lastPosition)&&const DeepCollectionEquality().equals(other._pointers, _pointers)&&(identical(other.buttons, buttons) || other.buttons == buttons)&&(identical(other.penDetected, penDetected) || other.penDetected == penDetected)&&(identical(other.sessionPenOnlyInput, sessionPenOnlyInput) || other.sessionPenOnlyInput == sessionPenOnlyInput)&&(identical(other.hideUi, hideUi) || other.hideUi == hideUi)); +} + + +@override +int get hashCode => Object.hash(runtimeType,lastPosition,const DeepCollectionEquality().hash(_pointers),buttons,penDetected,sessionPenOnlyInput,hideUi); + +@override +String toString() { + return 'EditorInputState(lastPosition: $lastPosition, pointers: $pointers, buttons: $buttons, penDetected: $penDetected, sessionPenOnlyInput: $sessionPenOnlyInput, hideUi: $hideUi)'; +} + + +} + +/// @nodoc +abstract mixin class _$EditorInputStateCopyWith<$Res> implements $EditorInputStateCopyWith<$Res> { + factory _$EditorInputStateCopyWith(_EditorInputState value, $Res Function(_EditorInputState) _then) = __$EditorInputStateCopyWithImpl; +@override @useResult +$Res call({ + Offset? lastPosition, List pointers, int? buttons, bool penDetected, bool sessionPenOnlyInput, HideState hideUi +}); + + + + +} +/// @nodoc +class __$EditorInputStateCopyWithImpl<$Res> + implements _$EditorInputStateCopyWith<$Res> { + __$EditorInputStateCopyWithImpl(this._self, this._then); + + final _EditorInputState _self; + final $Res Function(_EditorInputState) _then; + +/// Create a copy of EditorInputState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? lastPosition = freezed,Object? pointers = null,Object? buttons = freezed,Object? penDetected = null,Object? sessionPenOnlyInput = null,Object? hideUi = null,}) { + return _then(_EditorInputState( +lastPosition: freezed == lastPosition ? _self.lastPosition : lastPosition // ignore: cast_nullable_to_non_nullable +as Offset?,pointers: null == pointers ? _self._pointers : pointers // ignore: cast_nullable_to_non_nullable +as List,buttons: freezed == buttons ? _self.buttons : buttons // ignore: cast_nullable_to_non_nullable +as int?,penDetected: null == penDetected ? _self.penDetected : penDetected // ignore: cast_nullable_to_non_nullable +as bool,sessionPenOnlyInput: null == sessionPenOnlyInput ? _self.sessionPenOnlyInput : sessionPenOnlyInput // ignore: cast_nullable_to_non_nullable +as bool,hideUi: null == hideUi ? _self.hideUi : hideUi // ignore: cast_nullable_to_non_nullable +as HideState, + )); +} + + +} + +/// @nodoc +mixin _$DocumentSaveState { + + bool get isSaveDelayed; AssetLocation get location; Embedding? get embedding; SaveState get saved; bool get isCreating; +/// Create a copy of DocumentSaveState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$DocumentSaveStateCopyWith get copyWith => _$DocumentSaveStateCopyWithImpl(this as DocumentSaveState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is DocumentSaveState&&(identical(other.isSaveDelayed, isSaveDelayed) || other.isSaveDelayed == isSaveDelayed)&&(identical(other.location, location) || other.location == location)&&(identical(other.embedding, embedding) || other.embedding == embedding)&&(identical(other.saved, saved) || other.saved == saved)&&(identical(other.isCreating, isCreating) || other.isCreating == isCreating)); +} + + +@override +int get hashCode => Object.hash(runtimeType,isSaveDelayed,location,embedding,saved,isCreating); + +@override +String toString() { + return 'DocumentSaveState(isSaveDelayed: $isSaveDelayed, location: $location, embedding: $embedding, saved: $saved, isCreating: $isCreating)'; +} + + +} + +/// @nodoc +abstract mixin class $DocumentSaveStateCopyWith<$Res> { + factory $DocumentSaveStateCopyWith(DocumentSaveState value, $Res Function(DocumentSaveState) _then) = _$DocumentSaveStateCopyWithImpl; +@useResult +$Res call({ + bool isSaveDelayed, AssetLocation location, Embedding? embedding, SaveState saved, bool isCreating +}); + + + + +} +/// @nodoc +class _$DocumentSaveStateCopyWithImpl<$Res> + implements $DocumentSaveStateCopyWith<$Res> { + _$DocumentSaveStateCopyWithImpl(this._self, this._then); + + final DocumentSaveState _self; + final $Res Function(DocumentSaveState) _then; + +/// Create a copy of DocumentSaveState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? isSaveDelayed = null,Object? location = null,Object? embedding = freezed,Object? saved = null,Object? isCreating = null,}) { + return _then(_self.copyWith( +isSaveDelayed: null == isSaveDelayed ? _self.isSaveDelayed : isSaveDelayed // ignore: cast_nullable_to_non_nullable +as bool,location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable +as AssetLocation,embedding: freezed == embedding ? _self.embedding : embedding // ignore: cast_nullable_to_non_nullable +as Embedding?,saved: null == saved ? _self.saved : saved // ignore: cast_nullable_to_non_nullable +as SaveState,isCreating: null == isCreating ? _self.isCreating : isCreating // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + + +/// @nodoc + + +class _DocumentSaveState extends DocumentSaveState { + const _DocumentSaveState({this.isSaveDelayed = false, this.location = const AssetLocation(path: ''), this.embedding, this.saved = SaveState.saved, this.isCreating = false}): super._(); + + +@override@JsonKey() final bool isSaveDelayed; +@override@JsonKey() final AssetLocation location; +@override final Embedding? embedding; +@override@JsonKey() final SaveState saved; +@override@JsonKey() final bool isCreating; + +/// Create a copy of DocumentSaveState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$DocumentSaveStateCopyWith<_DocumentSaveState> get copyWith => __$DocumentSaveStateCopyWithImpl<_DocumentSaveState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _DocumentSaveState&&(identical(other.isSaveDelayed, isSaveDelayed) || other.isSaveDelayed == isSaveDelayed)&&(identical(other.location, location) || other.location == location)&&(identical(other.embedding, embedding) || other.embedding == embedding)&&(identical(other.saved, saved) || other.saved == saved)&&(identical(other.isCreating, isCreating) || other.isCreating == isCreating)); +} + + +@override +int get hashCode => Object.hash(runtimeType,isSaveDelayed,location,embedding,saved,isCreating); + +@override +String toString() { + return 'DocumentSaveState(isSaveDelayed: $isSaveDelayed, location: $location, embedding: $embedding, saved: $saved, isCreating: $isCreating)'; +} + + +} + +/// @nodoc +abstract mixin class _$DocumentSaveStateCopyWith<$Res> implements $DocumentSaveStateCopyWith<$Res> { + factory _$DocumentSaveStateCopyWith(_DocumentSaveState value, $Res Function(_DocumentSaveState) _then) = __$DocumentSaveStateCopyWithImpl; +@override @useResult +$Res call({ + bool isSaveDelayed, AssetLocation location, Embedding? embedding, SaveState saved, bool isCreating +}); + + + + +} +/// @nodoc +class __$DocumentSaveStateCopyWithImpl<$Res> + implements _$DocumentSaveStateCopyWith<$Res> { + __$DocumentSaveStateCopyWithImpl(this._self, this._then); + + final _DocumentSaveState _self; + final $Res Function(_DocumentSaveState) _then; + +/// Create a copy of DocumentSaveState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? isSaveDelayed = null,Object? location = null,Object? embedding = freezed,Object? saved = null,Object? isCreating = null,}) { + return _then(_DocumentSaveState( +isSaveDelayed: null == isSaveDelayed ? _self.isSaveDelayed : isSaveDelayed // ignore: cast_nullable_to_non_nullable +as bool,location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable +as AssetLocation,embedding: freezed == embedding ? _self.embedding : embedding // ignore: cast_nullable_to_non_nullable +as Embedding?,saved: null == saved ? _self.saved : saved // ignore: cast_nullable_to_non_nullable +as SaveState,isCreating: null == isCreating ? _self.isCreating : isCreating // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + +/// @nodoc +mixin _$EditorViewState { + + UtilitiesState get utilities; ViewOption get viewOption; bool get areaNavigatorCreate; bool get areaNavigatorExact; bool get areaNavigatorAsk; bool get navigatorEnabled; NavigatorPage get navigatorPage; String get userName; +/// Create a copy of EditorViewState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$EditorViewStateCopyWith get copyWith => _$EditorViewStateCopyWithImpl(this as EditorViewState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is EditorViewState&&(identical(other.utilities, utilities) || other.utilities == utilities)&&(identical(other.viewOption, viewOption) || other.viewOption == viewOption)&&(identical(other.areaNavigatorCreate, areaNavigatorCreate) || other.areaNavigatorCreate == areaNavigatorCreate)&&(identical(other.areaNavigatorExact, areaNavigatorExact) || other.areaNavigatorExact == areaNavigatorExact)&&(identical(other.areaNavigatorAsk, areaNavigatorAsk) || other.areaNavigatorAsk == areaNavigatorAsk)&&(identical(other.navigatorEnabled, navigatorEnabled) || other.navigatorEnabled == navigatorEnabled)&&(identical(other.navigatorPage, navigatorPage) || other.navigatorPage == navigatorPage)&&(identical(other.userName, userName) || other.userName == userName)); +} + + +@override +int get hashCode => Object.hash(runtimeType,utilities,viewOption,areaNavigatorCreate,areaNavigatorExact,areaNavigatorAsk,navigatorEnabled,navigatorPage,userName); + +@override +String toString() { + return 'EditorViewState(utilities: $utilities, viewOption: $viewOption, areaNavigatorCreate: $areaNavigatorCreate, areaNavigatorExact: $areaNavigatorExact, areaNavigatorAsk: $areaNavigatorAsk, navigatorEnabled: $navigatorEnabled, navigatorPage: $navigatorPage, userName: $userName)'; +} + + +} + +/// @nodoc +abstract mixin class $EditorViewStateCopyWith<$Res> { + factory $EditorViewStateCopyWith(EditorViewState value, $Res Function(EditorViewState) _then) = _$EditorViewStateCopyWithImpl; +@useResult +$Res call({ + UtilitiesState utilities, ViewOption viewOption, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, bool navigatorEnabled, NavigatorPage navigatorPage, String userName +}); + + +$UtilitiesStateCopyWith<$Res> get utilities;$ViewOptionCopyWith<$Res> get viewOption; + +} +/// @nodoc +class _$EditorViewStateCopyWithImpl<$Res> + implements $EditorViewStateCopyWith<$Res> { + _$EditorViewStateCopyWithImpl(this._self, this._then); + + final EditorViewState _self; + final $Res Function(EditorViewState) _then; + +/// Create a copy of EditorViewState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? utilities = null,Object? viewOption = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? userName = null,}) { + return _then(_self.copyWith( +utilities: null == utilities ? _self.utilities : utilities // ignore: cast_nullable_to_non_nullable +as UtilitiesState,viewOption: null == viewOption ? _self.viewOption : viewOption // ignore: cast_nullable_to_non_nullable +as ViewOption,areaNavigatorCreate: null == areaNavigatorCreate ? _self.areaNavigatorCreate : areaNavigatorCreate // ignore: cast_nullable_to_non_nullable +as bool,areaNavigatorExact: null == areaNavigatorExact ? _self.areaNavigatorExact : areaNavigatorExact // ignore: cast_nullable_to_non_nullable +as bool,areaNavigatorAsk: null == areaNavigatorAsk ? _self.areaNavigatorAsk : areaNavigatorAsk // ignore: cast_nullable_to_non_nullable +as bool,navigatorEnabled: null == navigatorEnabled ? _self.navigatorEnabled : navigatorEnabled // ignore: cast_nullable_to_non_nullable +as bool,navigatorPage: null == navigatorPage ? _self.navigatorPage : navigatorPage // ignore: cast_nullable_to_non_nullable +as NavigatorPage,userName: null == userName ? _self.userName : userName // ignore: cast_nullable_to_non_nullable +as String, + )); +} +/// Create a copy of EditorViewState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$UtilitiesStateCopyWith<$Res> get utilities { + + return $UtilitiesStateCopyWith<$Res>(_self.utilities, (value) { + return _then(_self.copyWith(utilities: value)); + }); +}/// Create a copy of EditorViewState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ViewOptionCopyWith<$Res> get viewOption { + + return $ViewOptionCopyWith<$Res>(_self.viewOption, (value) { + return _then(_self.copyWith(viewOption: value)); + }); +} +} + + + +/// @nodoc + + +class _EditorViewState implements EditorViewState { + const _EditorViewState({this.utilities = const UtilitiesState(), this.viewOption = const ViewOption(), this.areaNavigatorCreate = true, this.areaNavigatorExact = true, this.areaNavigatorAsk = false, this.navigatorEnabled = false, this.navigatorPage = NavigatorPage.waypoints, this.userName = ''}); + + +@override@JsonKey() final UtilitiesState utilities; +@override@JsonKey() final ViewOption viewOption; +@override@JsonKey() final bool areaNavigatorCreate; +@override@JsonKey() final bool areaNavigatorExact; +@override@JsonKey() final bool areaNavigatorAsk; +@override@JsonKey() final bool navigatorEnabled; +@override@JsonKey() final NavigatorPage navigatorPage; +@override@JsonKey() final String userName; + +/// Create a copy of EditorViewState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$EditorViewStateCopyWith<_EditorViewState> get copyWith => __$EditorViewStateCopyWithImpl<_EditorViewState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _EditorViewState&&(identical(other.utilities, utilities) || other.utilities == utilities)&&(identical(other.viewOption, viewOption) || other.viewOption == viewOption)&&(identical(other.areaNavigatorCreate, areaNavigatorCreate) || other.areaNavigatorCreate == areaNavigatorCreate)&&(identical(other.areaNavigatorExact, areaNavigatorExact) || other.areaNavigatorExact == areaNavigatorExact)&&(identical(other.areaNavigatorAsk, areaNavigatorAsk) || other.areaNavigatorAsk == areaNavigatorAsk)&&(identical(other.navigatorEnabled, navigatorEnabled) || other.navigatorEnabled == navigatorEnabled)&&(identical(other.navigatorPage, navigatorPage) || other.navigatorPage == navigatorPage)&&(identical(other.userName, userName) || other.userName == userName)); +} + + +@override +int get hashCode => Object.hash(runtimeType,utilities,viewOption,areaNavigatorCreate,areaNavigatorExact,areaNavigatorAsk,navigatorEnabled,navigatorPage,userName); + +@override +String toString() { + return 'EditorViewState(utilities: $utilities, viewOption: $viewOption, areaNavigatorCreate: $areaNavigatorCreate, areaNavigatorExact: $areaNavigatorExact, areaNavigatorAsk: $areaNavigatorAsk, navigatorEnabled: $navigatorEnabled, navigatorPage: $navigatorPage, userName: $userName)'; +} + + +} + +/// @nodoc +abstract mixin class _$EditorViewStateCopyWith<$Res> implements $EditorViewStateCopyWith<$Res> { + factory _$EditorViewStateCopyWith(_EditorViewState value, $Res Function(_EditorViewState) _then) = __$EditorViewStateCopyWithImpl; +@override @useResult +$Res call({ + UtilitiesState utilities, ViewOption viewOption, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, bool navigatorEnabled, NavigatorPage navigatorPage, String userName +}); + + +@override $UtilitiesStateCopyWith<$Res> get utilities;@override $ViewOptionCopyWith<$Res> get viewOption; + +} +/// @nodoc +class __$EditorViewStateCopyWithImpl<$Res> + implements _$EditorViewStateCopyWith<$Res> { + __$EditorViewStateCopyWithImpl(this._self, this._then); + + final _EditorViewState _self; + final $Res Function(_EditorViewState) _then; + +/// Create a copy of EditorViewState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? utilities = null,Object? viewOption = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? userName = null,}) { + return _then(_EditorViewState( +utilities: null == utilities ? _self.utilities : utilities // ignore: cast_nullable_to_non_nullable +as UtilitiesState,viewOption: null == viewOption ? _self.viewOption : viewOption // ignore: cast_nullable_to_non_nullable +as ViewOption,areaNavigatorCreate: null == areaNavigatorCreate ? _self.areaNavigatorCreate : areaNavigatorCreate // ignore: cast_nullable_to_non_nullable +as bool,areaNavigatorExact: null == areaNavigatorExact ? _self.areaNavigatorExact : areaNavigatorExact // ignore: cast_nullable_to_non_nullable +as bool,areaNavigatorAsk: null == areaNavigatorAsk ? _self.areaNavigatorAsk : areaNavigatorAsk // ignore: cast_nullable_to_non_nullable +as bool,navigatorEnabled: null == navigatorEnabled ? _self.navigatorEnabled : navigatorEnabled // ignore: cast_nullable_to_non_nullable +as bool,navigatorPage: null == navigatorPage ? _self.navigatorPage : navigatorPage // ignore: cast_nullable_to_non_nullable +as NavigatorPage,userName: null == userName ? _self.userName : userName // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +/// Create a copy of EditorViewState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$UtilitiesStateCopyWith<$Res> get utilities { + + return $UtilitiesStateCopyWith<$Res>(_self.utilities, (value) { + return _then(_self.copyWith(utilities: value)); + }); +}/// Create a copy of EditorViewState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ViewOptionCopyWith<$Res> get viewOption { + + return $ViewOptionCopyWith<$Res>(_self.viewOption, (value) { + return _then(_self.copyWith(viewOption: value)); + }); +} +} + +// dart format on diff --git a/app/lib/cubits/editor_session.dart b/app/lib/cubits/editor_session.dart new file mode 100644 index 000000000000..6a5509d5d30f --- /dev/null +++ b/app/lib/cubits/editor_session.dart @@ -0,0 +1,220 @@ +import 'dart:async'; + +import 'package:butterfly/api/file_system.dart'; +import 'package:butterfly/cubits/transform.dart'; +import 'package:butterfly/models/persisted_document_state.dart'; +import 'package:butterfly/views/navigator/view.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class EditorSessionCubit extends Cubit { + EditorSessionCubit({ + required this.fileSystem, + required TransformCubit transformCubit, + required PersistedDocumentState initialState, + required this.pathKey, + required this.contentHash, + this.persist = true, + }) : _transformCubit = transformCubit, + super(initialState.touch(pathKey: pathKey, contentHash: contentHash)) { + _transformSubscription = transformCubit.stream.listen(_onTransformChanged); + } + + final DocumentStateFileSystem fileSystem; + final TransformCubit _transformCubit; + final String? pathKey; + final String? contentHash; + final bool persist; + StreamSubscription? _transformSubscription; + Timer? _saveDebounce; + + static Future load({ + required DocumentStateFileSystem fileSystem, + String? contentHash, + String? pathKey, + bool allowContentHash = true, + }) async { + await fileSystem.initialize(); + if (allowContentHash && contentHash != null) { + final byContent = await fileSystem.getFile( + documentStateContentKey(contentHash), + ); + if (byContent != null) return byContent; + } + if (pathKey != null) { + return fileSystem.getFile(pathKey); + } + return null; + } + + static PersistedDocumentState buildInitial({ + PersistedDocumentState? restored, + required NoteData document, + required DocumentPage page, + required String? fallbackPageName, + required UtilitiesState fallbackUtilities, + String? pathKey, + String? contentHash, + }) { + final pages = document.getPages(true).toSet(); + final pageName = + restored?.pageName != null && pages.contains(restored!.pageName) + ? restored.pageName + : fallbackPageName; + final layers = page.layers.map((e) => e.id).nonNulls.toSet(); + final currentLayer = + restored?.currentLayer != null && + restored!.currentLayer.isNotEmpty && + layers.contains(restored.currentLayer) + ? restored.currentLayer + : page.layers.lastOrNull?.id ?? ''; + final invisibleLayers = + restored?.invisibleLayers.where(layers.contains).toSet() ?? + const {}; + return (restored ?? PersistedDocumentState(utilities: fallbackUtilities)) + .copyWith( + pathKey: pathKey, + contentHash: contentHash, + pageName: pageName, + currentLayer: currentLayer, + invisibleLayers: invisibleLayers, + updatedAt: restored?.updatedAt, + ); + } + + CameraTransform get cameraTransform => CameraTransform( + _transformCubit.state.pixelRatio, + Offset(state.camera.positionX, state.camera.positionY), + state.camera.zoom, + ); + + int resolveToolIndex(DocumentInfo info) { + final toolId = state.selectedTool.toolId; + if (toolId != null) { + final index = info.tools.indexWhere((tool) => tool.id == toolId); + if (index >= 0) return index; + } + final index = state.selectedTool.toolIndex; + if (index != null && index >= 0 && index < info.tools.length) { + return index; + } + return 0; + } + + NavigatorPage get navigatorPage => + NavigatorPage.values.firstWhereOrNull( + (e) => e.name == state.navigatorPage, + ) ?? + NavigatorPage.waypoints; + + void _onTransformChanged(CameraTransform transform) { + final camera = PersistedCameraState( + positionX: transform.position.dx, + positionY: transform.position.dy, + zoom: transform.size, + ); + if (state.camera == camera) return; + emit(state.copyWith(camera: camera)); + scheduleSave(); + } + + void updatePage(String pageName) { + if (state.pageName == pageName) return; + emit(state.copyWith(pageName: pageName)); + unawaited(saveNow()); + } + + void updateUtilities(UtilitiesState utilities) { + if (state.utilities == utilities) return; + emit(state.copyWith(utilities: utilities)); + unawaited(saveNow()); + } + + void updateSelectedTool(Tool? tool, int? index) { + final selection = PersistedToolSelection( + toolId: tool?.id, + toolIndex: index, + ); + if (state.selectedTool == selection) return; + emit(state.copyWith(selectedTool: selection)); + unawaited(saveNow()); + } + + void updateNavigator({bool? enabled, NavigatorPage? page}) { + final next = state.copyWith( + navigatorEnabled: enabled ?? state.navigatorEnabled, + navigatorPage: page?.name ?? state.navigatorPage, + ); + if (next == state) return; + emit(next); + unawaited(saveNow()); + } + + void updateLayer({ + String? currentLayer, + String? currentCollection, + Set? invisibleLayers, + }) { + final next = state.copyWith( + currentLayer: currentLayer ?? state.currentLayer, + currentCollection: currentCollection ?? state.currentCollection, + invisibleLayers: invisibleLayers ?? state.invisibleLayers, + ); + if (next == state) return; + emit(next); + unawaited(saveNow()); + } + + void updateAreaNavigator({bool? create, bool? exact, bool? ask}) { + final next = state.copyWith( + areaNavigatorCreate: create ?? state.areaNavigatorCreate, + areaNavigatorExact: exact ?? state.areaNavigatorExact, + areaNavigatorAsk: ask ?? state.areaNavigatorAsk, + ); + if (next == state) return; + emit(next); + unawaited(saveNow()); + } + + void scheduleSave() { + _saveDebounce?.cancel(); + _saveDebounce = Timer(const Duration(milliseconds: 250), () { + unawaited(saveNow()); + }); + } + + Future saveNow() async { + if (!persist) return; + _saveDebounce?.cancel(); + _saveDebounce = null; + final next = state.touch(pathKey: pathKey, contentHash: contentHash); + emit(next); + await fileSystem.initialize(); + if (contentHash != null) { + final key = documentStateContentKey(contentHash!); + if (await fileSystem.hasKey(key)) { + await fileSystem.updateFile(key, next); + } else { + await fileSystem.createFile(key, next); + } + } + if (pathKey != null) { + if (await fileSystem.hasKey(pathKey!)) { + await fileSystem.updateFile(pathKey!, next); + } else { + await fileSystem.createFile(pathKey!, next); + } + } + } + + @override + Future close() async { + _saveDebounce?.cancel(); + _saveDebounce = null; + if (persist) await saveNow(); + await _transformSubscription?.cancel(); + return super.close(); + } +} diff --git a/app/lib/dialogs/area/context.dart b/app/lib/dialogs/area/context.dart index a59de84dc2c1..c4c8a7b32f70 100644 --- a/app/lib/dialogs/area/context.dart +++ b/app/lib/dialogs/area/context.dart @@ -1,5 +1,5 @@ import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/dialogs/layers.dart'; import 'package:butterfly/dialogs/pages.dart'; @@ -26,7 +26,7 @@ ContextMenuBuilder buildAreaContextMenu( bool includeRenameAndEnterArea = true, String? pageName, }) => (context) { - final cubit = bloc.currentIndexCubit; + final cubit = bloc.editorController; final areaPageName = pageName ?? state.pageName; return [ if (includeRenameAndEnterArea) ...[ @@ -99,7 +99,7 @@ ContextMenuBuilder buildAreaContextMenu( ContextMenuItem( onPressed: () { if (pop) Navigator.of(context).pop(true); - cubit.changeSelection(area); + cubit.toolCubit.changeSelection(area); }, icon: const PhosphorIcon(PhosphorIconsLight.faders), label: AppLocalizations.of(context).properties, @@ -114,8 +114,8 @@ ContextMenuBuilder buildAreaContextMenu( ]; }; -List _getAreaElements(CurrentIndexCubit cubit, Area area) { - return cubit.renderers +List _getAreaElements(EditorController cubit, Area area) { + return cubit.rendererCubit.renderers .where((e) => e.area == area) .map( (e) => e.transform(position: -area.position.toOffset(), relative: true), diff --git a/app/lib/dialogs/collaboration/dialog.dart b/app/lib/dialogs/collaboration/dialog.dart index c5175e4c0369..1d1baa06c262 100644 --- a/app/lib/dialogs/collaboration/dialog.dart +++ b/app/lib/dialogs/collaboration/dialog.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:butterfly/api/open.dart'; import 'package:butterfly/api/save.dart'; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/services/network.dart'; import 'package:butterfly_api/butterfly_api.dart'; @@ -22,16 +22,16 @@ part 'view.dart'; Future showCollaborationDialog(BuildContext context) { final bloc = context.read(); - final currentIndexCubit = context.read(); + final editorController = context.read(); return showDialog( context: context, builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: bloc), - BlocProvider.value(value: currentIndexCubit), - ], - child: const CollaborationDialog(), + providers: [BlocProvider.value(value: bloc)], + child: RepositoryProvider.value( + value: editorController, + child: const CollaborationDialog(), + ), ), ); } @@ -41,7 +41,7 @@ class CollaborationDialog extends StatelessWidget { @override Widget build(BuildContext context) { - final cubit = context.read(); + final cubit = context.read(); final service = cubit.networkingService; return BlocBuilder( bloc: service, @@ -51,7 +51,7 @@ class CollaborationDialog extends StatelessWidget { return ViewCollaborationDialog( state: state, service: service, - currentIndexCubit: cubit, + editorController: cubit, ); } else { return StartCollaborationDialog(service: service); diff --git a/app/lib/dialogs/collaboration/view.dart b/app/lib/dialogs/collaboration/view.dart index 41a2cdaaa22a..775dd1f64004 100644 --- a/app/lib/dialogs/collaboration/view.dart +++ b/app/lib/dialogs/collaboration/view.dart @@ -3,13 +3,13 @@ part of 'dialog.dart'; class ViewCollaborationDialog extends StatelessWidget { final NetworkingService service; final NetworkState state; - final CurrentIndexCubit currentIndexCubit; + final EditorController editorController; const ViewCollaborationDialog({ super.key, required this.service, required this.state, - required this.currentIndexCubit, + required this.editorController, }); @override @@ -86,9 +86,9 @@ class ViewCollaborationDialog extends StatelessWidget { labelText: AppLocalizations.of(context).username, filled: true, ), - initialValue: currentIndexCubit.state.userName, + initialValue: editorController.viewCubit.state.userName, onChanged: (value) { - currentIndexCubit.setUserName(value); + editorController.viewCubit.setUserName(value); }, ), ], diff --git a/app/lib/dialogs/collections.dart b/app/lib/dialogs/collections.dart index d2deff2e1187..5601239424c9 100644 --- a/app/lib/dialogs/collections.dart +++ b/app/lib/dialogs/collections.dart @@ -1,5 +1,5 @@ import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/dialogs/delete.dart'; import 'package:butterfly/dialogs/layers.dart'; import 'package:butterfly/handlers/handler.dart'; @@ -96,7 +96,7 @@ class _CollectionsDialogState extends State { onPressed: () async { final bloc = context.read(); Navigator.pop(context); - final cubit = bloc.currentIndexCubit; + final cubit = bloc.editorController; final handler = cubit.fetchHandler() ?? await cubit.changeTemporaryHandler( diff --git a/app/lib/dialogs/elements.dart b/app/lib/dialogs/elements.dart index af96a6fe0491..95b68b0afa9c 100644 --- a/app/lib/dialogs/elements.dart +++ b/app/lib/dialogs/elements.dart @@ -12,6 +12,7 @@ import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../../renderers/renderer.dart'; import '../bloc/document_bloc.dart'; +import '../cubits/editor_controller.dart'; import '../services/import.dart'; ContextMenuBuilder buildElementsContextMenu( @@ -24,7 +25,7 @@ ContextMenuBuilder buildElementsContextMenu( List> renderers, Rect? rect, ) { - final cubit = bloc.currentIndexCubit; + final cubit = bloc.editorController; final settingsCubit = state.settingsCubit; final operations = Map< @@ -178,8 +179,8 @@ ContextMenuBuilder buildElementsContextMenu( onPressed: () { Navigator.of(context).pop(true); if (renderers.isEmpty) return; - cubit.changeSelection(renderers.first); - renderers.sublist(1).forEach((r) => cubit.insertSelection(r)); + cubit.toolCubit.changeSelection(renderers.first); + renderers.sublist(1).forEach((r) => cubit.toolCubit.insertSelection(r)); }, icon: const PhosphorIcon(PhosphorIconsLight.faders), label: AppLocalizations.of(context).properties, diff --git a/app/lib/dialogs/export/general.dart b/app/lib/dialogs/export/general.dart index a3418cbe9efa..02bb5dac43d9 100644 --- a/app/lib/dialogs/export/general.dart +++ b/app/lib/dialogs/export/general.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:butterfly/api/save.dart'; import 'package:butterfly/bloc/document_bloc.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter/foundation.dart'; @@ -98,7 +99,7 @@ class _GeneralExportDialogState extends State { ); } } - return bloc.currentIndexCubit.render( + return bloc.editorController.render( state.data, state.page, state.info, @@ -112,7 +113,7 @@ class _GeneralExportDialogState extends State { final bloc = context.read(); final state = bloc.state; if (state is! DocumentLoaded) return null; - return bloc.currentIndexCubit + return bloc.editorController .renderSVG( state.data, state.page, @@ -279,7 +280,7 @@ class _GeneralExportDialogState extends State { onPressed: () { final transform = context .read() - .currentIndexCubit + .editorController .transformCubit .state; @@ -308,7 +309,7 @@ class _GeneralExportDialogState extends State { final bloc = context.read(); final state = bloc.state; if (state is! DocumentLoaded) return; - final cubit = bloc.currentIndexCubit; + final cubit = bloc.editorController; final rect = cubit.getPageRect( invisibleLayers: state.invisibleLayers, ); diff --git a/app/lib/dialogs/export/pdf.dart b/app/lib/dialogs/export/pdf.dart index 860badf1dca5..62aeb85025dc 100644 --- a/app/lib/dialogs/export/pdf.dart +++ b/app/lib/dialogs/export/pdf.dart @@ -1,7 +1,7 @@ import 'dart:math'; import 'package:butterfly/api/save.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/dialogs/load.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter/foundation.dart'; @@ -50,7 +50,7 @@ class _PdfExportDialogState extends State { if (state is! DocumentLoadSuccess) { return const Center(child: CircularProgressIndicator()); } - final currentIndex = context.read().currentIndexCubit; + final currentIndex = context.read().editorController; return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -219,14 +219,11 @@ class _PdfExportDialogState extends State { if (state is! DocumentLoadSuccess) return; final loading = showLoadingDialog(context); try { - final pdf = await context - .read() - .currentIndexCubit - .renderPDF( - state, - areas: _areas.map((e) => e.preset).toList(), - onProgress: (progress) => loading?.setProgress(progress), - ); + final pdf = await context.read().editorController.renderPDF( + state, + areas: _areas.map((e) => e.preset).toList(), + onProgress: (progress) => loading?.setProgress(progress), + ); if (pdf == null) { throw Exception('Failed to generate PDF.'); } @@ -338,7 +335,7 @@ class _PdfExportDialogState extends State { ); } - Widget _buildAreasList(DocumentLoaded state, CurrentIndexCubit currentIndex) { + Widget _buildAreasList(DocumentLoaded state, EditorController currentIndex) { return ReorderableListView.builder( buildDefaultDragHandles: false, itemCount: _areas.length, @@ -391,7 +388,7 @@ class _AreaPreview extends StatefulWidget { final AreaPreset preset; final DocumentPage page; final DocumentLoaded state; - final CurrentIndexCubit currentIndex; + final EditorController currentIndex; final VoidCallback onRemove; final ValueChanged onQualityChanged; diff --git a/app/lib/dialogs/export/thumbnail.dart b/app/lib/dialogs/export/thumbnail.dart index fff215a91906..45a52d0e348d 100644 --- a/app/lib/dialogs/export/thumbnail.dart +++ b/app/lib/dialogs/export/thumbnail.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:butterfly/bloc/document_bloc.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/view_painter.dart'; import 'package:butterfly_api/butterfly_api.dart'; @@ -104,7 +105,8 @@ class _ThumbnailCaptureDialogState extends State { Widget build(BuildContext context) { final cameraViewport = context .read() - .currentIndexCubit + .editorController + .rendererCubit .state .cameraViewport; return ResponsiveAlertDialog( @@ -510,16 +512,15 @@ class _ThumbnailCaptureDialogState extends State { scale: scale, ); - final currentIndexCubit = context.read().currentIndexCubit; - final thumbnail = await currentIndexCubit.render( + final editorController = context.read().editorController; + final thumbnail = await editorController.render( widget.state.data, widget.state.page, widget.state.info, options, invisibleLayers: widget.state.invisibleLayers, - cameraViewport: currentIndexCubit.state.cameraViewport.unbake( - unbakedElements: currentIndexCubit.renderers, - ), + cameraViewport: editorController.rendererCubit.state.cameraViewport + .unbake(unbakedElements: editorController.rendererCubit.renderers), docState: widget.state, ); diff --git a/app/lib/dialogs/import/add.dart b/app/lib/dialogs/import/add.dart index 2c7c530f73ed..f51ac6afe92c 100644 --- a/app/lib/dialogs/import/add.dart +++ b/app/lib/dialogs/import/add.dart @@ -1,5 +1,5 @@ import 'package:butterfly/api/file_system.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/handlers/handler.dart'; import 'package:butterfly/helpers/color.dart'; @@ -265,7 +265,7 @@ class _AddDialogState extends State { Widget _buildBody(ButterflySettings settings) { final bloc = context.read(); - final currentIndexCubit = context.read(); + final editorController = context.read(); final favorites = settings.favoriteTools; final settingsCubit = context.read(); @@ -287,7 +287,7 @@ class _AddDialogState extends State { bloc.add(ToolCreated(defaultTool)); if (!defaultTool.isAction()) { - currentIndexCubit.changeTool( + editorController.changeTool( bloc, index: state.info.tools.length, context: context, diff --git a/app/lib/dialogs/packs/asset.dart b/app/lib/dialogs/packs/asset.dart index 36a9aeba0ac6..c29a2da1915f 100644 --- a/app/lib/dialogs/packs/asset.dart +++ b/app/lib/dialogs/packs/asset.dart @@ -12,6 +12,7 @@ import 'package:material_leap/l10n/leap_localizations.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../../bloc/document_bloc.dart'; +import '../../cubits/editor_controller.dart'; import 'pack.dart'; class AssetDialog extends StatelessWidget { @@ -146,7 +147,7 @@ Future addToPack( if (result == null) return; var pack = await packSystem.getFile(result.namespace); if (pack == null) return; - final screenshot = await bloc.currentIndexCubit.render( + final screenshot = await bloc.editorController.render( state.data, state.page, state.info, diff --git a/app/lib/embed/handler.dart b/app/lib/embed/handler.dart index c6eb06b5f587..3d60412c6e4a 100644 --- a/app/lib/embed/handler.dart +++ b/app/lib/embed/handler.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -83,7 +83,7 @@ class EmbedHandler { void register(BuildContext context, DocumentBloc bloc) { _blocSubscription ??= bloc.stream.listen((state) { if (state is DocumentLoadSuccess && - bloc.currentIndexCubit.state.saved == SaveState.unsaved) { + bloc.editorController.saveCubit.state.saved == SaveState.unsaved) { _changeDebounceTimer?.cancel(); _changeDebounceTimer = Timer( const Duration(milliseconds: 500), @@ -92,10 +92,7 @@ class EmbedHandler { if (currentState is DocumentLoadSuccess) { sendEmbedMessage( 'change', - (await currentState.saveData( - null, - bloc.currentIndexCubit.state.viewOption, - )).exportAsBytes(), + (await currentState.saveData()).exportAsBytes(), ); } }, @@ -106,13 +103,7 @@ class EmbedHandler { getDataListener ??= onEmbedMessage('getData', (message) async { final state = bloc.state; if (state is DocumentLoadSuccess) { - sendEmbedMessage( - 'getData', - (await state.saveData( - null, - bloc.currentIndexCubit.state.viewOption, - )).exportAsBytes(), - ); + sendEmbedMessage('getData', (await state.saveData()).exportAsBytes()); } }); setDataListener ??= onEmbedMessage('setData', (message) async { @@ -125,7 +116,7 @@ class EmbedHandler { }); return; } - final embedding = bloc.currentIndexCubit.state.embedding; + final embedding = bloc.editorController.saveCubit.state.embedding; if (embedding == null) return; GoRouter.of( context, @@ -146,7 +137,7 @@ class EmbedHandler { scale = _mapDouble(map, 'scale', 1); renderBackground = _mapBool(map, 'renderBackground', true); } - final data = await bloc.currentIndexCubit.render( + final data = await bloc.editorController.render( state.data, state.page, state.info, @@ -182,7 +173,7 @@ class EmbedHandler { } sendEmbedMessage( 'renderSVG', - bloc.currentIndexCubit + bloc.editorController .renderSVG( state.data, state.page, diff --git a/app/lib/handlers/area.dart b/app/lib/handlers/area.dart index c7a737d93178..d026eb05b422 100644 --- a/app/lib/handlers/area.dart +++ b/app/lib/handlers/area.dart @@ -61,7 +61,7 @@ class AreaHandler extends Handler { @override List createForegrounds( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, NoteData document, DocumentPage page, DocumentInfo info, [ @@ -115,15 +115,16 @@ class AreaHandler extends Handler { @override bool onScaleStart(ScaleStartDetails details, EventContext context) { - final currentIndex = context.getCurrentIndex(); - if (currentIndex.buttons == kSecondaryMouseButton && - currentIndex.temporaryHandler == null) { + final toolState = context.getToolState(); + final inputState = context.getInputState(); + if (inputState.buttons == kSecondaryMouseButton && + toolState.temporaryHandler == null) { return true; } final transform = context.getCameraTransform(); var localPos = details.localFocalPoint; localPos = PointerManipulationHandler.calculatePointerPosition( - currentIndex, + toolState, localPos, context.viewportSize, transform, @@ -150,7 +151,7 @@ class AreaHandler extends Handler { @override void onScaleUpdate(ScaleUpdateDetails details, EventContext context) { final transform = context.getCameraTransform(); - final currentIndex = context.getCurrentIndex(); + final currentIndex = context.getToolState(); var localPos = details.localFocalPoint; localPos = PointerManipulationHandler.calculatePointerPosition( currentIndex, diff --git a/app/lib/handlers/barcode.dart b/app/lib/handlers/barcode.dart index f4306e6ddc60..decfbd8a1f9e 100644 --- a/app/lib/handlers/barcode.dart +++ b/app/lib/handlers/barcode.dart @@ -46,7 +46,7 @@ class BarcodeHandler extends PastingHandler List transformElements( Rect rect, String collection, - CurrentIndexCubit cubit, + EditorController cubit, ) { final element = _element; if (element == null) return []; diff --git a/app/lib/handlers/eraser.dart b/app/lib/handlers/eraser.dart index bbeec5a77a3c..6b788787c4b1 100644 --- a/app/lib/handlers/eraser.dart +++ b/app/lib/handlers/eraser.dart @@ -28,7 +28,7 @@ class EraserHandler extends Handler { @override List createForegrounds( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, NoteData document, DocumentPage page, DocumentInfo info, [ @@ -95,9 +95,9 @@ class EraserHandler extends Handler { } Future _eraseAt(Offset position, EventContext context) async { - final cubit = context.getCurrentIndexCubit(); + final cubit = context.getEditorController(); final transform = cubit.transformCubit.state; - final utilities = cubit.state.utilities; + final utilities = cubit.viewCubit.state.utilities; final globalPos = transform.localToGlobal(position); final size = data.strokeWidth; final sizeSquared = size * size; diff --git a/app/lib/handlers/eye_dropper.dart b/app/lib/handlers/eye_dropper.dart index 78f5bb876ce8..c39e1a11dc57 100644 --- a/app/lib/handlers/eye_dropper.dart +++ b/app/lib/handlers/eye_dropper.dart @@ -9,7 +9,7 @@ class EyeDropperHandler extends Handler { bool wasAdded = true, ]) { if (!wasAdded) { - context.read().changeTemporaryHandler( + context.read().changeTemporaryHandler( context, data, temporaryState: TemporaryState.removeAfterRelease, @@ -25,7 +25,7 @@ class EyeDropperHandler extends Handler { ); final state = context.getState(); if (state == null) return; - final data = await context.getCurrentIndexCubit().render( + final data = await context.getEditorController().render( state.data, state.page, state.info, @@ -36,7 +36,7 @@ class EyeDropperHandler extends Handler { final image = img.decodePng(data.buffer.asUint8List()); if (image == null) return; final pixel = image.getPixel(0, 0); - final handler = context.getCurrentIndexCubit().getHandler( + final handler = context.getEditorController().getHandler( disableTemporary: true, ); final color = SRGBColor.from( diff --git a/app/lib/handlers/grid.dart b/app/lib/handlers/grid.dart index f834eda427aa..43e268a699db 100644 --- a/app/lib/handlers/grid.dart +++ b/app/lib/handlers/grid.dart @@ -5,7 +5,7 @@ class GridHandler extends Handler with PointerManipulationHandler { @override List createForegrounds( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, NoteData document, DocumentPage page, DocumentInfo info, [ diff --git a/app/lib/handlers/handler.dart b/app/lib/handlers/handler.dart index 0500a82fa1dc..e67bd66e7e6c 100644 --- a/app/lib/handlers/handler.dart +++ b/app/lib/handlers/handler.dart @@ -43,7 +43,7 @@ import 'package:share_plus/share_plus.dart'; import '../actions/paste.dart'; import '../actions/select.dart'; import '../api/save.dart'; -import '../cubits/current_index.dart'; +import '../cubits/editor_controller.dart'; import '../dialogs/import/camera.dart'; import '../models/label.dart'; import '../models/viewport.dart'; @@ -52,6 +52,7 @@ import '../renderers/cursors/label.dart'; import '../renderers/renderer.dart'; import '../services/asset.dart'; import '../services/import.dart'; +import '../theme.dart'; import '../views/toolbar/color.dart'; import '../views/toolbar/components.dart'; import '../views/toolbar/label.dart'; @@ -125,10 +126,18 @@ class EventContext { CameraTransform getCameraTransform() => getTransformCubit().state; - CurrentIndexCubit getCurrentIndexCubit() => - BlocProvider.of(buildContext); + EditorController getEditorController() => + buildContext.read(); - CurrentIndex getCurrentIndex() => getCurrentIndexCubit().state; + RendererRuntimeState getRendererState() => + buildContext.read().state; + + ToolRuntimeState getToolState() => buildContext.read().state; + + EditorInputState getInputState() => + buildContext.read().state; + + EditorViewState getViewState() => buildContext.read().state; Future refresh({bool allowBake = true}) => getDocumentBloc().refresh(allowBake: allowBake); @@ -170,7 +179,6 @@ class EventContext { List getProviders() => [ BlocProvider.value(value: getDocumentBloc()), BlocProvider.value(value: getTransformCubit()), - BlocProvider.value(value: getCurrentIndexCubit()), BlocProvider.value(value: getSettingsCubit()), ]; @@ -183,7 +191,7 @@ class EventContext { ClipboardManager getClipboardManager() => buildContext.read(); - CameraViewport getCameraViewport() => getCurrentIndex().cameraViewport; + CameraViewport getCameraViewport() => getRendererState().cameraViewport; NoteData? getData() => getState()?.data; @@ -209,7 +217,7 @@ abstract class Handler { ]) => SelectState.normal; List createForegrounds( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, NoteData document, DocumentPage page, DocumentInfo info, [ diff --git a/app/lib/handlers/import.dart b/app/lib/handlers/import.dart index c9610b5f743b..ae1718a5361f 100644 --- a/app/lib/handlers/import.dart +++ b/app/lib/handlers/import.dart @@ -102,7 +102,7 @@ class ImportHandler extends Handler { @override List createForegrounds( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, NoteData document, DocumentPage page, DocumentInfo info, [ diff --git a/app/lib/handlers/label.dart b/app/lib/handlers/label.dart index 6595e3d747f0..0348a67f777d 100644 --- a/app/lib/handlers/label.dart +++ b/app/lib/handlers/label.dart @@ -82,14 +82,14 @@ class LabelHandler extends Handler @override List createForegrounds( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, NoteData document, DocumentPage page, DocumentInfo info, [ Area? currentArea, ]) => [ ...super.createForegrounds( - currentIndexCubit, + editorController, document, page, info, @@ -172,7 +172,7 @@ class LabelHandler extends Handler Offset localPosition, [ bool forceCreate = false, ]) async { - final currentIndex = context.getCurrentIndex(); + final currentIndex = context.getToolState(); localPosition = PointerManipulationHandler.calculatePointerPosition( currentIndex, localPosition, @@ -193,7 +193,7 @@ class LabelHandler extends Handler final style = theme.textTheme.bodyLarge!; if (!hit || forceCreate || _context?.element == null) { if (_context?.element != null && !hit) _submit(context.getDocumentBloc()); - final utilities = currentIndex.utilities; + final utilities = context.getViewState().utilities; final hits = forceCreate ? >{} : await context.getDocumentBloc().rayCast( diff --git a/app/lib/handlers/laser.dart b/app/lib/handlers/laser.dart index daa7bae2d033..74be883b0c27 100644 --- a/app/lib/handlers/laser.dart +++ b/app/lib/handlers/laser.dart @@ -73,7 +73,7 @@ class LaserHandler extends Handler with ColoredHandler { @override List createForegrounds( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, NoteData document, DocumentPage page, DocumentInfo info, [ @@ -129,12 +129,12 @@ class LaserHandler extends Handler with ColoredHandler { bool forceCreate = false, }) { final bloc = context.read(); - final currentIndexCubit = context.read(); + final editorController = context.read(); final transform = context.read().state; final state = bloc.state as DocumentLoadSuccess; - final penOnlyInput = currentIndexCubit.effectivePenOnlyInput; + final penOnlyInput = editorController.inputCubit.effectivePenOnlyInput; localPosition = PointerManipulationHandler.calculatePointerPosition( - currentIndexCubit.state, + editorController.toolCubit.state, localPosition, viewportSize, transform, @@ -176,8 +176,9 @@ class LaserHandler extends Handler with ColoredHandler { changeStartedDrawing(context); _hideCursorWhileDrawing = context.getSettings().hideCursorWhileDrawing; context.refreshForegrounds(); - final cubit = context.getCurrentIndexCubit(); - if (cubit.moveEnabled && event.kind != PointerDeviceKind.stylus) { + final cubit = context.getEditorController(); + if (cubit.inputCubit.moveEnabled && + event.kind != PointerDeviceKind.stylus) { _elements.clear(); return; } diff --git a/app/lib/handlers/mixins.dart b/app/lib/handlers/mixins.dart index 6b8c7f816c46..f81f584df9b3 100644 --- a/app/lib/handlers/mixins.dart +++ b/app/lib/handlers/mixins.dart @@ -11,7 +11,7 @@ mixin ColoredHandler on Handler { void changeStartedDrawing(EventContext context) { if (_startedDrawing) return; _startedDrawing = true; - context.getCurrentIndexCubit().refreshToolbar(context.getDocumentBloc()); + context.getEditorController().refreshToolbar(context.getDocumentBloc()); } @override @@ -25,7 +25,7 @@ mixin ColoredHandler on Handler { color: getColor(), onChanged: (value) => changeToolColor(bloc, value), onEyeDropper: (context) { - bloc.currentIndexCubit.changeTemporaryHandler( + bloc.editorController.changeTemporaryHandler( context, EyeDropperTool(), bloc: bloc, @@ -52,14 +52,14 @@ mixin HandlerWithCursor on Handler { @mustCallSuper @override List createForegrounds( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, NoteData document, DocumentPage page, DocumentInfo info, [ Area? currentArea, ]) { final renderers = super.createForegrounds( - currentIndexCubit, + editorController, document, page, info, @@ -100,26 +100,26 @@ abstract class PastingHandler extends Handler { @override List createForegrounds( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, NoteData document, DocumentPage page, DocumentInfo info, [ Area? currentArea, ]) => [ if (_firstPos != null && _secondPos != null) - ...getTransformed(currentIndexCubit).map((e) => Renderer.fromInstance(e)), + ...getTransformed(editorController).map((e) => Renderer.fromInstance(e)), if (_firstPos == null && showHoverPreview && _hoverPos != null) ...transformElements( Rect.fromPoints(_hoverPos!, _hoverPos!), _currentCollection, - currentIndexCubit, + editorController, ).map(Renderer.fromInstance), ]; List transformElements( Rect rect, String collection, - CurrentIndexCubit cubit, + EditorController cubit, ); @protected @@ -130,7 +130,7 @@ abstract class PastingHandler extends Handler { bool get shouldNormalize => true; - List getTransformed(CurrentIndexCubit cubit) { + List getTransformed(EditorController cubit) { final first = _firstPos; final second = _secondPos; if (first == null || second == null) return []; @@ -195,7 +195,7 @@ abstract class PastingHandler extends Handler { Offset _getGlobalPosition(Offset localPosition, EventContext context) { final transform = context.getCameraTransform(); var localPos = localPosition; - final currentIndex = context.getCurrentIndex(); + final currentIndex = context.getToolState(); final viewportSize = context.viewportSize; localPos = PointerManipulationHandler.calculatePointerPosition( currentIndex, @@ -266,7 +266,7 @@ abstract class PastingHandler extends Handler { } void _createElements(DocumentBloc bloc, EventContext context) { - final elements = getTransformed(bloc.currentIndexCubit); + final elements = getTransformed(bloc.editorController); if (elements.isEmpty) return; final current = List.from(elements); bloc.add(ElementsCreated(current)); @@ -302,7 +302,7 @@ mixin PointerManipulationHandler on Handler { } static Offset calculatePointerPosition( - CurrentIndex index, + ToolRuntimeState index, Offset position, Size viewportSize, [ CameraTransform transform = const CameraTransform(), diff --git a/app/lib/handlers/pen.dart b/app/lib/handlers/pen.dart index 805a8050a87b..b629e5f5b92a 100644 --- a/app/lib/handlers/pen.dart +++ b/app/lib/handlers/pen.dart @@ -24,7 +24,7 @@ class PenHandler extends Handler with ColoredHandler { // Create foregrounds for rendering the PenRendere @override List createForegrounds( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, NoteData document, DocumentPage page, DocumentInfo info, [ @@ -146,10 +146,10 @@ class PenHandler extends Handler with ColoredHandler { bool shouldCreate = false, }) { final bloc = context.read(); - final currentIndexCubit = context.read(); + final editorController = context.read(); final transform = context.read().state; localPos = PointerManipulationHandler.calculatePointerPosition( - currentIndexCubit.state, + editorController.toolCubit.state, localPos, viewportSize, transform, @@ -158,7 +158,7 @@ class PenHandler extends Handler with ColoredHandler { if (!bloc.isInBounds(globalPos)) return; final state = bloc.state as DocumentLoadSuccess; final settings = context.read().state; - final penOnlyInput = currentIndexCubit.effectivePenOnlyInput; + final penOnlyInput = editorController.inputCubit.effectivePenOnlyInput; if (lastPosition[pointer] == localPos) return; lastPosition[pointer] = localPos; if (penOnlyInput && @@ -202,12 +202,13 @@ class PenHandler extends Handler with ColoredHandler { // This function is called when the pointer is pressed down. @override void onPointerDown(PointerDownEvent event, EventContext context) { - final cubit = context.getCurrentIndexCubit(); + final cubit = context.getEditorController(); cubit.cancelDelayedBake(); isDrawing = true; changeStartedDrawing(context); _hideCursorWhileDrawing = context.getSettings().hideCursorWhileDrawing; - if (cubit.moveEnabled && event.kind != PointerDeviceKind.stylus) { + if (cubit.inputCubit.moveEnabled && + event.kind != PointerDeviceKind.stylus) { elements.clear(); context.refreshForegrounds(); return; diff --git a/app/lib/handlers/polygon.dart b/app/lib/handlers/polygon.dart index 732fc2f8e847..d986711da1c6 100644 --- a/app/lib/handlers/polygon.dart +++ b/app/lib/handlers/polygon.dart @@ -77,7 +77,7 @@ class PolygonHandler extends Handler with ColoredHandler { @override List createForegrounds( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, NoteData document, DocumentPage page, DocumentInfo info, [ @@ -313,7 +313,7 @@ class PolygonHandler extends Handler with ColoredHandler { final globalPos = transform.localToGlobal(localPos); if (_element == null) { - final utilities = context.getCurrentIndex().utilities; + final utilities = context.getViewState().utilities; final hit = await context.getDocumentBloc().rayCast( globalPos, max( @@ -404,7 +404,7 @@ class PolygonHandler extends Handler with ColoredHandler { bloc.add(ElementsCreated([element])); } _resetTool(); - bloc.currentIndexCubit.resetTemporaryHandler(bloc, true); + bloc.editorController.resetTemporaryHandler(bloc, true); bloc.delayedBake(); bloc.refreshToolbar(); } @@ -425,7 +425,7 @@ class PolygonHandler extends Handler with ColoredHandler { _resetTool(); bloc.refreshForegrounds(); bloc.refreshToolbar(); - bloc.currentIndexCubit.resetTemporaryHandler(bloc, true); + bloc.editorController.resetTemporaryHandler(bloc, true); return; } @@ -438,7 +438,7 @@ class PolygonHandler extends Handler with ColoredHandler { bloc.refresh(); } _resetTool(); - bloc.currentIndexCubit.resetTemporaryHandler(bloc, true); + bloc.editorController.resetTemporaryHandler(bloc, true); } else { _selectedPointIndex = max(0, selectedIndex - 1); } diff --git a/app/lib/handlers/presentation.dart b/app/lib/handlers/presentation.dart index c51eb5451d45..e9569628c3a1 100644 --- a/app/lib/handlers/presentation.dart +++ b/app/lib/handlers/presentation.dart @@ -81,7 +81,7 @@ mixin GeneralPresentationHandler { _applyAnimation( animation, bloc, - bloc.currentIndexCubit, + bloc.editorController, bloc.transformCubit, ); @@ -130,7 +130,7 @@ mixin GeneralPresentationHandler { void _applyAnimation( AnimationTrack animation, DocumentBloc bloc, - CurrentIndexCubit cubit, + EditorController cubit, TransformCubit transformCubit, ) { final state = bloc.state; @@ -151,7 +151,7 @@ mixin GeneralPresentationHandler { _applyAnimation( animation, bloc, - bloc.currentIndexCubit, + bloc.editorController, bloc.transformCubit, ); } @@ -226,7 +226,7 @@ class PresentationHandler extends GeneralHandHandler void _refreshToolbar(DocumentBloc bloc) { final state = bloc.state; if (state is! DocumentLoaded) return; - bloc.currentIndexCubit.refreshToolbar(bloc); + bloc.editorController.refreshToolbar(bloc); } } diff --git a/app/lib/handlers/ruler.dart b/app/lib/handlers/ruler.dart index 8a6768e807eb..7bd15647ece0 100644 --- a/app/lib/handlers/ruler.dart +++ b/app/lib/handlers/ruler.dart @@ -44,7 +44,7 @@ class RulerHandler extends Handler with PointerManipulationHandler { @override List createForegrounds( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, NoteData document, DocumentPage page, DocumentInfo info, [ @@ -151,7 +151,7 @@ class RulerHandler extends Handler with PointerManipulationHandler { } static RulerHandler? getFirstRuler( - CurrentIndex index, + ToolRuntimeState index, Offset position, Size viewportSize, ) { @@ -161,7 +161,7 @@ class RulerHandler extends Handler with PointerManipulationHandler { } static RulerHandler? getInteractiveRuler( - CurrentIndex index, + ToolRuntimeState index, Handler handler, Offset position, Size viewportSize, diff --git a/app/lib/handlers/select.dart b/app/lib/handlers/select.dart index d3fc7d43ce37..1cf1b10c0e44 100644 --- a/app/lib/handlers/select.dart +++ b/app/lib/handlers/select.dart @@ -170,7 +170,7 @@ class SelectHandler extends Handler { @override List createForegrounds( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, NoteData document, DocumentPage page, DocumentInfo info, [ @@ -185,7 +185,8 @@ class SelectHandler extends Handler { (_selectionManager.isTransforming && !_duplicate ? _selected : []), ); final selectionRect = getSelectionRect(); - final scheme = currentIndexCubit.getTheme(false).colorScheme; + final settings = editorController.settingsCubit.state; + final scheme = getThemeData(settings.design, false).colorScheme; if (selectionRect != null) { foregrounds.add(_selectionManager.renderer); } @@ -271,7 +272,7 @@ class SelectHandler extends Handler { if (_selectionManager.isTransforming) { return; } - final utilities = context.getCurrentIndex().utilities; + final utilities = context.getViewState().utilities; final transform = context.getCameraTransform(); final globalPos = transform.localToGlobal(localPosition); final selectionRect = getSelectionRect(); @@ -328,7 +329,7 @@ class SelectHandler extends Handler { final bloc = context.getDocumentBloc(); final state = bloc.state; if (state is! DocumentLoadSuccess) return; - final utilities = context.getCurrentIndex().utilities; + final utilities = context.getViewState().utilities; final hits = await bloc.rayCast( position, 0.0, @@ -373,9 +374,10 @@ class SelectHandler extends Handler { @override bool onScaleStart(ScaleStartDetails details, EventContext context) { - final currentIndex = context.getCurrentIndex(); - if (currentIndex.buttons == kSecondaryMouseButton && - currentIndex.temporaryHandler == null) { + final toolState = context.getToolState(); + final inputState = context.getInputState(); + if (inputState.buttons == kSecondaryMouseButton && + toolState.temporaryHandler == null) { return false; } final cameraTransform = context.getCameraTransform(); @@ -454,7 +456,7 @@ class SelectHandler extends Handler { @override void onScaleEnd(ScaleEndDetails details, EventContext context) async { - final utilities = context.getCurrentIndex().utilities; + final utilities = context.getViewState().utilities; final rectangleSelection = _rectangleFreeSelection?.normalized(); final lassoSelection = _lassoFreeSelection; final transformed = _submitTransform(context.getDocumentBloc()); @@ -562,7 +564,7 @@ class SelectHandler extends Handler { if (state is! DocumentLoadSuccess) return; _selected.clear(); _selected.addAll( - bloc.currentIndexCubit.renderers.where((e) => filter?.call(e) ?? true), + bloc.editorController.rendererCubit.renderers.where((e) => filter?.call(e) ?? true), ); _updateSelectionRect(); bloc.refreshForegrounds(); diff --git a/app/lib/handlers/shape.dart b/app/lib/handlers/shape.dart index 1bfb0c4be8c2..c7cc6d9cc72c 100644 --- a/app/lib/handlers/shape.dart +++ b/app/lib/handlers/shape.dart @@ -17,7 +17,7 @@ class ShapeHandler extends PastingHandler with ColoredHandler { List transformElements( Rect rect, String collection, - CurrentIndexCubit cubit, + EditorController cubit, ) { if (rect.topLeft == rect.bottomRight) return []; @@ -42,7 +42,7 @@ class ShapeHandler extends PastingHandler with ColoredHandler { property: data.property.copyWith( strokeWidth: data.property.strokeWidth / - (data.zoomDependent ? cubit.state.cameraViewport.scale : 1), + (data.zoomDependent ? cubit.rendererCubit.state.cameraViewport.scale : 1), ), collection: collection, ), diff --git a/app/lib/handlers/spacer.dart b/app/lib/handlers/spacer.dart index 7c8bff6cd428..396f0e4460b1 100644 --- a/app/lib/handlers/spacer.dart +++ b/app/lib/handlers/spacer.dart @@ -9,7 +9,7 @@ class SpacerHandler extends Handler { @override List createForegrounds( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, NoteData document, DocumentPage page, DocumentInfo info, [ diff --git a/app/lib/handlers/stamp.dart b/app/lib/handlers/stamp.dart index 51baf3bfb411..460f06e641ba 100644 --- a/app/lib/handlers/stamp.dart +++ b/app/lib/handlers/stamp.dart @@ -10,7 +10,7 @@ class StampHandler extends PastingHandler { final state = context.getState(); if (state == null) return; await _loadComponent( - context.getCurrentIndexCubit().transformCubit, + context.getEditorController().transformCubit, state.data, state.assetService, state.page, @@ -62,7 +62,7 @@ class StampHandler extends PastingHandler { List transformElements( Rect rect, String collection, - CurrentIndexCubit cubit, + EditorController cubit, ) { final elements = _elements; if (elements == null || elements.isEmpty) return []; diff --git a/app/lib/handlers/texture.dart b/app/lib/handlers/texture.dart index 72a939903de9..5f0777c7fc61 100644 --- a/app/lib/handlers/texture.dart +++ b/app/lib/handlers/texture.dart @@ -7,7 +7,7 @@ class TextureHandler extends PastingHandler { List transformElements( Rect rect, String collection, - CurrentIndexCubit cubit, + EditorController cubit, ) { if (rect.isEmpty) return []; diff --git a/app/lib/models/persisted_document_state.dart b/app/lib/models/persisted_document_state.dart new file mode 100644 index 000000000000..754bcf3224e0 --- /dev/null +++ b/app/lib/models/persisted_document_state.dart @@ -0,0 +1,114 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:crypto/crypto.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lw_file_system/lw_file_system.dart'; + +part 'persisted_document_state.freezed.dart'; +part 'persisted_document_state.g.dart'; + +const kPersistedDocumentStateVersion = 1; + +String documentStateContentKey(String contentHash) => 'content/$contentHash'; + +String documentStatePathKey(AssetLocation location) { + final normalized = _normalizeDocumentStatePath(location.path); + final bytes = utf8.encode('${location.remote}:$normalized'); + return 'path/${base64Url.encode(bytes)}'; +} + +String? documentStatePathKeyOrNull(AssetLocation? location) { + if (location == null || location.path.isEmpty) return null; + return documentStatePathKey(location); +} + +String documentStateContentHash(Uint8List bytes) => + sha512256.convert(bytes).toString(); + +String _normalizeDocumentStatePath(String path) { + path = path.replaceAll('\\', '/'); + while (path.contains('//')) { + path = path.replaceAll('//', '/'); + } + if (path.endsWith('/') && path.length > 1) { + path = path.substring(0, path.length - 1); + } + if (path.isNotEmpty && !path.startsWith('/')) { + path = '/$path'; + } + return path; +} + +@freezed +sealed class PersistedToolSelection with _$PersistedToolSelection { + const factory PersistedToolSelection({String? toolId, int? toolIndex}) = + _PersistedToolSelection; + + factory PersistedToolSelection.fromJson(Map json) => + _$PersistedToolSelectionFromJson(json); +} + +@freezed +sealed class PersistedCameraState with _$PersistedCameraState { + const factory PersistedCameraState({ + @Default(0) double positionX, + @Default(0) double positionY, + @Default(1) double zoom, + }) = _PersistedCameraState; + + factory PersistedCameraState.fromJson(Map json) => + _$PersistedCameraStateFromJson(json); +} + +@freezed +sealed class PersistedDocumentState with _$PersistedDocumentState { + const PersistedDocumentState._(); + + const factory PersistedDocumentState({ + @Default(kPersistedDocumentStateVersion) int version, + String? pathKey, + String? contentHash, + String? pageName, + @Default(PersistedCameraState()) PersistedCameraState camera, + @Default(UtilitiesState()) UtilitiesState utilities, + @Default(PersistedToolSelection()) PersistedToolSelection selectedTool, + @Default(false) bool navigatorEnabled, + @Default('waypoints') String navigatorPage, + @Default('') String currentLayer, + @Default('') String currentCollection, + @Default({}) Set invisibleLayers, + @Default(true) bool areaNavigatorCreate, + @Default(true) bool areaNavigatorExact, + @Default(false) bool areaNavigatorAsk, + DateTime? updatedAt, + }) = _PersistedDocumentState; + + factory PersistedDocumentState.fromJson(Map json) => + _$PersistedDocumentStateFromJson(json); + + PersistedDocumentState touch({ + String? pathKey, + String? contentHash, + DateTime? now, + }) => copyWith( + pathKey: pathKey ?? this.pathKey, + contentHash: contentHash ?? this.contentHash, + updatedAt: now ?? DateTime.now().toUtc(), + ); +} + +Uint8List encodePersistedDocumentState(PersistedDocumentState state) => + Uint8List.fromList(utf8.encode(json.encode(state.toJson()))); + +PersistedDocumentState decodePersistedDocumentState(Uint8List bytes) { + final decoded = json.decode(utf8.decode(bytes)); + if (decoded is Map) { + return PersistedDocumentState.fromJson(decoded); + } + if (decoded is Map) { + return PersistedDocumentState.fromJson(decoded.cast()); + } + throw const FormatException('Invalid persisted document state'); +} diff --git a/app/lib/models/persisted_document_state.freezed.dart b/app/lib/models/persisted_document_state.freezed.dart new file mode 100644 index 000000000000..2c70e1c440f3 --- /dev/null +++ b/app/lib/models/persisted_document_state.freezed.dart @@ -0,0 +1,530 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'persisted_document_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$PersistedToolSelection { + + String? get toolId; int? get toolIndex; +/// Create a copy of PersistedToolSelection +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$PersistedToolSelectionCopyWith get copyWith => _$PersistedToolSelectionCopyWithImpl(this as PersistedToolSelection, _$identity); + + /// Serializes this PersistedToolSelection to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is PersistedToolSelection&&(identical(other.toolId, toolId) || other.toolId == toolId)&&(identical(other.toolIndex, toolIndex) || other.toolIndex == toolIndex)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,toolId,toolIndex); + +@override +String toString() { + return 'PersistedToolSelection(toolId: $toolId, toolIndex: $toolIndex)'; +} + + +} + +/// @nodoc +abstract mixin class $PersistedToolSelectionCopyWith<$Res> { + factory $PersistedToolSelectionCopyWith(PersistedToolSelection value, $Res Function(PersistedToolSelection) _then) = _$PersistedToolSelectionCopyWithImpl; +@useResult +$Res call({ + String? toolId, int? toolIndex +}); + + + + +} +/// @nodoc +class _$PersistedToolSelectionCopyWithImpl<$Res> + implements $PersistedToolSelectionCopyWith<$Res> { + _$PersistedToolSelectionCopyWithImpl(this._self, this._then); + + final PersistedToolSelection _self; + final $Res Function(PersistedToolSelection) _then; + +/// Create a copy of PersistedToolSelection +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? toolId = freezed,Object? toolIndex = freezed,}) { + return _then(_self.copyWith( +toolId: freezed == toolId ? _self.toolId : toolId // ignore: cast_nullable_to_non_nullable +as String?,toolIndex: freezed == toolIndex ? _self.toolIndex : toolIndex // ignore: cast_nullable_to_non_nullable +as int?, + )); +} + +} + + + +/// @nodoc +@JsonSerializable() + +class _PersistedToolSelection implements PersistedToolSelection { + const _PersistedToolSelection({this.toolId, this.toolIndex}); + factory _PersistedToolSelection.fromJson(Map json) => _$PersistedToolSelectionFromJson(json); + +@override final String? toolId; +@override final int? toolIndex; + +/// Create a copy of PersistedToolSelection +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$PersistedToolSelectionCopyWith<_PersistedToolSelection> get copyWith => __$PersistedToolSelectionCopyWithImpl<_PersistedToolSelection>(this, _$identity); + +@override +Map toJson() { + return _$PersistedToolSelectionToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _PersistedToolSelection&&(identical(other.toolId, toolId) || other.toolId == toolId)&&(identical(other.toolIndex, toolIndex) || other.toolIndex == toolIndex)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,toolId,toolIndex); + +@override +String toString() { + return 'PersistedToolSelection(toolId: $toolId, toolIndex: $toolIndex)'; +} + + +} + +/// @nodoc +abstract mixin class _$PersistedToolSelectionCopyWith<$Res> implements $PersistedToolSelectionCopyWith<$Res> { + factory _$PersistedToolSelectionCopyWith(_PersistedToolSelection value, $Res Function(_PersistedToolSelection) _then) = __$PersistedToolSelectionCopyWithImpl; +@override @useResult +$Res call({ + String? toolId, int? toolIndex +}); + + + + +} +/// @nodoc +class __$PersistedToolSelectionCopyWithImpl<$Res> + implements _$PersistedToolSelectionCopyWith<$Res> { + __$PersistedToolSelectionCopyWithImpl(this._self, this._then); + + final _PersistedToolSelection _self; + final $Res Function(_PersistedToolSelection) _then; + +/// Create a copy of PersistedToolSelection +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? toolId = freezed,Object? toolIndex = freezed,}) { + return _then(_PersistedToolSelection( +toolId: freezed == toolId ? _self.toolId : toolId // ignore: cast_nullable_to_non_nullable +as String?,toolIndex: freezed == toolIndex ? _self.toolIndex : toolIndex // ignore: cast_nullable_to_non_nullable +as int?, + )); +} + + +} + + +/// @nodoc +mixin _$PersistedCameraState { + + double get positionX; double get positionY; double get zoom; +/// Create a copy of PersistedCameraState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$PersistedCameraStateCopyWith get copyWith => _$PersistedCameraStateCopyWithImpl(this as PersistedCameraState, _$identity); + + /// Serializes this PersistedCameraState to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is PersistedCameraState&&(identical(other.positionX, positionX) || other.positionX == positionX)&&(identical(other.positionY, positionY) || other.positionY == positionY)&&(identical(other.zoom, zoom) || other.zoom == zoom)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,positionX,positionY,zoom); + +@override +String toString() { + return 'PersistedCameraState(positionX: $positionX, positionY: $positionY, zoom: $zoom)'; +} + + +} + +/// @nodoc +abstract mixin class $PersistedCameraStateCopyWith<$Res> { + factory $PersistedCameraStateCopyWith(PersistedCameraState value, $Res Function(PersistedCameraState) _then) = _$PersistedCameraStateCopyWithImpl; +@useResult +$Res call({ + double positionX, double positionY, double zoom +}); + + + + +} +/// @nodoc +class _$PersistedCameraStateCopyWithImpl<$Res> + implements $PersistedCameraStateCopyWith<$Res> { + _$PersistedCameraStateCopyWithImpl(this._self, this._then); + + final PersistedCameraState _self; + final $Res Function(PersistedCameraState) _then; + +/// Create a copy of PersistedCameraState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? positionX = null,Object? positionY = null,Object? zoom = null,}) { + return _then(_self.copyWith( +positionX: null == positionX ? _self.positionX : positionX // ignore: cast_nullable_to_non_nullable +as double,positionY: null == positionY ? _self.positionY : positionY // ignore: cast_nullable_to_non_nullable +as double,zoom: null == zoom ? _self.zoom : zoom // ignore: cast_nullable_to_non_nullable +as double, + )); +} + +} + + + +/// @nodoc +@JsonSerializable() + +class _PersistedCameraState implements PersistedCameraState { + const _PersistedCameraState({this.positionX = 0, this.positionY = 0, this.zoom = 1}); + factory _PersistedCameraState.fromJson(Map json) => _$PersistedCameraStateFromJson(json); + +@override@JsonKey() final double positionX; +@override@JsonKey() final double positionY; +@override@JsonKey() final double zoom; + +/// Create a copy of PersistedCameraState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$PersistedCameraStateCopyWith<_PersistedCameraState> get copyWith => __$PersistedCameraStateCopyWithImpl<_PersistedCameraState>(this, _$identity); + +@override +Map toJson() { + return _$PersistedCameraStateToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _PersistedCameraState&&(identical(other.positionX, positionX) || other.positionX == positionX)&&(identical(other.positionY, positionY) || other.positionY == positionY)&&(identical(other.zoom, zoom) || other.zoom == zoom)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,positionX,positionY,zoom); + +@override +String toString() { + return 'PersistedCameraState(positionX: $positionX, positionY: $positionY, zoom: $zoom)'; +} + + +} + +/// @nodoc +abstract mixin class _$PersistedCameraStateCopyWith<$Res> implements $PersistedCameraStateCopyWith<$Res> { + factory _$PersistedCameraStateCopyWith(_PersistedCameraState value, $Res Function(_PersistedCameraState) _then) = __$PersistedCameraStateCopyWithImpl; +@override @useResult +$Res call({ + double positionX, double positionY, double zoom +}); + + + + +} +/// @nodoc +class __$PersistedCameraStateCopyWithImpl<$Res> + implements _$PersistedCameraStateCopyWith<$Res> { + __$PersistedCameraStateCopyWithImpl(this._self, this._then); + + final _PersistedCameraState _self; + final $Res Function(_PersistedCameraState) _then; + +/// Create a copy of PersistedCameraState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? positionX = null,Object? positionY = null,Object? zoom = null,}) { + return _then(_PersistedCameraState( +positionX: null == positionX ? _self.positionX : positionX // ignore: cast_nullable_to_non_nullable +as double,positionY: null == positionY ? _self.positionY : positionY // ignore: cast_nullable_to_non_nullable +as double,zoom: null == zoom ? _self.zoom : zoom // ignore: cast_nullable_to_non_nullable +as double, + )); +} + + +} + + +/// @nodoc +mixin _$PersistedDocumentState { + + int get version; String? get pathKey; String? get contentHash; String? get pageName; PersistedCameraState get camera; UtilitiesState get utilities; PersistedToolSelection get selectedTool; bool get navigatorEnabled; String get navigatorPage; String get currentLayer; String get currentCollection; Set get invisibleLayers; bool get areaNavigatorCreate; bool get areaNavigatorExact; bool get areaNavigatorAsk; DateTime? get updatedAt; +/// Create a copy of PersistedDocumentState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$PersistedDocumentStateCopyWith get copyWith => _$PersistedDocumentStateCopyWithImpl(this as PersistedDocumentState, _$identity); + + /// Serializes this PersistedDocumentState to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is PersistedDocumentState&&(identical(other.version, version) || other.version == version)&&(identical(other.pathKey, pathKey) || other.pathKey == pathKey)&&(identical(other.contentHash, contentHash) || other.contentHash == contentHash)&&(identical(other.pageName, pageName) || other.pageName == pageName)&&(identical(other.camera, camera) || other.camera == camera)&&(identical(other.utilities, utilities) || other.utilities == utilities)&&(identical(other.selectedTool, selectedTool) || other.selectedTool == selectedTool)&&(identical(other.navigatorEnabled, navigatorEnabled) || other.navigatorEnabled == navigatorEnabled)&&(identical(other.navigatorPage, navigatorPage) || other.navigatorPage == navigatorPage)&&(identical(other.currentLayer, currentLayer) || other.currentLayer == currentLayer)&&(identical(other.currentCollection, currentCollection) || other.currentCollection == currentCollection)&&const DeepCollectionEquality().equals(other.invisibleLayers, invisibleLayers)&&(identical(other.areaNavigatorCreate, areaNavigatorCreate) || other.areaNavigatorCreate == areaNavigatorCreate)&&(identical(other.areaNavigatorExact, areaNavigatorExact) || other.areaNavigatorExact == areaNavigatorExact)&&(identical(other.areaNavigatorAsk, areaNavigatorAsk) || other.areaNavigatorAsk == areaNavigatorAsk)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,version,pathKey,contentHash,pageName,camera,utilities,selectedTool,navigatorEnabled,navigatorPage,currentLayer,currentCollection,const DeepCollectionEquality().hash(invisibleLayers),areaNavigatorCreate,areaNavigatorExact,areaNavigatorAsk,updatedAt); + +@override +String toString() { + return 'PersistedDocumentState(version: $version, pathKey: $pathKey, contentHash: $contentHash, pageName: $pageName, camera: $camera, utilities: $utilities, selectedTool: $selectedTool, navigatorEnabled: $navigatorEnabled, navigatorPage: $navigatorPage, currentLayer: $currentLayer, currentCollection: $currentCollection, invisibleLayers: $invisibleLayers, areaNavigatorCreate: $areaNavigatorCreate, areaNavigatorExact: $areaNavigatorExact, areaNavigatorAsk: $areaNavigatorAsk, updatedAt: $updatedAt)'; +} + + +} + +/// @nodoc +abstract mixin class $PersistedDocumentStateCopyWith<$Res> { + factory $PersistedDocumentStateCopyWith(PersistedDocumentState value, $Res Function(PersistedDocumentState) _then) = _$PersistedDocumentStateCopyWithImpl; +@useResult +$Res call({ + int version, String? pathKey, String? contentHash, String? pageName, PersistedCameraState camera, UtilitiesState utilities, PersistedToolSelection selectedTool, bool navigatorEnabled, String navigatorPage, String currentLayer, String currentCollection, Set invisibleLayers, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, DateTime? updatedAt +}); + + +$PersistedCameraStateCopyWith<$Res> get camera;$UtilitiesStateCopyWith<$Res> get utilities;$PersistedToolSelectionCopyWith<$Res> get selectedTool; + +} +/// @nodoc +class _$PersistedDocumentStateCopyWithImpl<$Res> + implements $PersistedDocumentStateCopyWith<$Res> { + _$PersistedDocumentStateCopyWithImpl(this._self, this._then); + + final PersistedDocumentState _self; + final $Res Function(PersistedDocumentState) _then; + +/// Create a copy of PersistedDocumentState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? version = null,Object? pathKey = freezed,Object? contentHash = freezed,Object? pageName = freezed,Object? camera = null,Object? utilities = null,Object? selectedTool = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? currentLayer = null,Object? currentCollection = null,Object? invisibleLayers = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? updatedAt = freezed,}) { + return _then(_self.copyWith( +version: null == version ? _self.version : version // ignore: cast_nullable_to_non_nullable +as int,pathKey: freezed == pathKey ? _self.pathKey : pathKey // ignore: cast_nullable_to_non_nullable +as String?,contentHash: freezed == contentHash ? _self.contentHash : contentHash // ignore: cast_nullable_to_non_nullable +as String?,pageName: freezed == pageName ? _self.pageName : pageName // ignore: cast_nullable_to_non_nullable +as String?,camera: null == camera ? _self.camera : camera // ignore: cast_nullable_to_non_nullable +as PersistedCameraState,utilities: null == utilities ? _self.utilities : utilities // ignore: cast_nullable_to_non_nullable +as UtilitiesState,selectedTool: null == selectedTool ? _self.selectedTool : selectedTool // ignore: cast_nullable_to_non_nullable +as PersistedToolSelection,navigatorEnabled: null == navigatorEnabled ? _self.navigatorEnabled : navigatorEnabled // ignore: cast_nullable_to_non_nullable +as bool,navigatorPage: null == navigatorPage ? _self.navigatorPage : navigatorPage // ignore: cast_nullable_to_non_nullable +as String,currentLayer: null == currentLayer ? _self.currentLayer : currentLayer // ignore: cast_nullable_to_non_nullable +as String,currentCollection: null == currentCollection ? _self.currentCollection : currentCollection // ignore: cast_nullable_to_non_nullable +as String,invisibleLayers: null == invisibleLayers ? _self.invisibleLayers : invisibleLayers // ignore: cast_nullable_to_non_nullable +as Set,areaNavigatorCreate: null == areaNavigatorCreate ? _self.areaNavigatorCreate : areaNavigatorCreate // ignore: cast_nullable_to_non_nullable +as bool,areaNavigatorExact: null == areaNavigatorExact ? _self.areaNavigatorExact : areaNavigatorExact // ignore: cast_nullable_to_non_nullable +as bool,areaNavigatorAsk: null == areaNavigatorAsk ? _self.areaNavigatorAsk : areaNavigatorAsk // ignore: cast_nullable_to_non_nullable +as bool,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable +as DateTime?, + )); +} +/// Create a copy of PersistedDocumentState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$PersistedCameraStateCopyWith<$Res> get camera { + + return $PersistedCameraStateCopyWith<$Res>(_self.camera, (value) { + return _then(_self.copyWith(camera: value)); + }); +}/// Create a copy of PersistedDocumentState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$UtilitiesStateCopyWith<$Res> get utilities { + + return $UtilitiesStateCopyWith<$Res>(_self.utilities, (value) { + return _then(_self.copyWith(utilities: value)); + }); +}/// Create a copy of PersistedDocumentState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$PersistedToolSelectionCopyWith<$Res> get selectedTool { + + return $PersistedToolSelectionCopyWith<$Res>(_self.selectedTool, (value) { + return _then(_self.copyWith(selectedTool: value)); + }); +} +} + + + +/// @nodoc +@JsonSerializable() + +class _PersistedDocumentState extends PersistedDocumentState { + const _PersistedDocumentState({this.version = kPersistedDocumentStateVersion, this.pathKey, this.contentHash, this.pageName, this.camera = const PersistedCameraState(), this.utilities = const UtilitiesState(), this.selectedTool = const PersistedToolSelection(), this.navigatorEnabled = false, this.navigatorPage = 'waypoints', this.currentLayer = '', this.currentCollection = '', final Set invisibleLayers = const {}, this.areaNavigatorCreate = true, this.areaNavigatorExact = true, this.areaNavigatorAsk = false, this.updatedAt}): _invisibleLayers = invisibleLayers,super._(); + factory _PersistedDocumentState.fromJson(Map json) => _$PersistedDocumentStateFromJson(json); + +@override@JsonKey() final int version; +@override final String? pathKey; +@override final String? contentHash; +@override final String? pageName; +@override@JsonKey() final PersistedCameraState camera; +@override@JsonKey() final UtilitiesState utilities; +@override@JsonKey() final PersistedToolSelection selectedTool; +@override@JsonKey() final bool navigatorEnabled; +@override@JsonKey() final String navigatorPage; +@override@JsonKey() final String currentLayer; +@override@JsonKey() final String currentCollection; + final Set _invisibleLayers; +@override@JsonKey() Set get invisibleLayers { + if (_invisibleLayers is EqualUnmodifiableSetView) return _invisibleLayers; + // ignore: implicit_dynamic_type + return EqualUnmodifiableSetView(_invisibleLayers); +} + +@override@JsonKey() final bool areaNavigatorCreate; +@override@JsonKey() final bool areaNavigatorExact; +@override@JsonKey() final bool areaNavigatorAsk; +@override final DateTime? updatedAt; + +/// Create a copy of PersistedDocumentState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$PersistedDocumentStateCopyWith<_PersistedDocumentState> get copyWith => __$PersistedDocumentStateCopyWithImpl<_PersistedDocumentState>(this, _$identity); + +@override +Map toJson() { + return _$PersistedDocumentStateToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _PersistedDocumentState&&(identical(other.version, version) || other.version == version)&&(identical(other.pathKey, pathKey) || other.pathKey == pathKey)&&(identical(other.contentHash, contentHash) || other.contentHash == contentHash)&&(identical(other.pageName, pageName) || other.pageName == pageName)&&(identical(other.camera, camera) || other.camera == camera)&&(identical(other.utilities, utilities) || other.utilities == utilities)&&(identical(other.selectedTool, selectedTool) || other.selectedTool == selectedTool)&&(identical(other.navigatorEnabled, navigatorEnabled) || other.navigatorEnabled == navigatorEnabled)&&(identical(other.navigatorPage, navigatorPage) || other.navigatorPage == navigatorPage)&&(identical(other.currentLayer, currentLayer) || other.currentLayer == currentLayer)&&(identical(other.currentCollection, currentCollection) || other.currentCollection == currentCollection)&&const DeepCollectionEquality().equals(other._invisibleLayers, _invisibleLayers)&&(identical(other.areaNavigatorCreate, areaNavigatorCreate) || other.areaNavigatorCreate == areaNavigatorCreate)&&(identical(other.areaNavigatorExact, areaNavigatorExact) || other.areaNavigatorExact == areaNavigatorExact)&&(identical(other.areaNavigatorAsk, areaNavigatorAsk) || other.areaNavigatorAsk == areaNavigatorAsk)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,version,pathKey,contentHash,pageName,camera,utilities,selectedTool,navigatorEnabled,navigatorPage,currentLayer,currentCollection,const DeepCollectionEquality().hash(_invisibleLayers),areaNavigatorCreate,areaNavigatorExact,areaNavigatorAsk,updatedAt); + +@override +String toString() { + return 'PersistedDocumentState(version: $version, pathKey: $pathKey, contentHash: $contentHash, pageName: $pageName, camera: $camera, utilities: $utilities, selectedTool: $selectedTool, navigatorEnabled: $navigatorEnabled, navigatorPage: $navigatorPage, currentLayer: $currentLayer, currentCollection: $currentCollection, invisibleLayers: $invisibleLayers, areaNavigatorCreate: $areaNavigatorCreate, areaNavigatorExact: $areaNavigatorExact, areaNavigatorAsk: $areaNavigatorAsk, updatedAt: $updatedAt)'; +} + + +} + +/// @nodoc +abstract mixin class _$PersistedDocumentStateCopyWith<$Res> implements $PersistedDocumentStateCopyWith<$Res> { + factory _$PersistedDocumentStateCopyWith(_PersistedDocumentState value, $Res Function(_PersistedDocumentState) _then) = __$PersistedDocumentStateCopyWithImpl; +@override @useResult +$Res call({ + int version, String? pathKey, String? contentHash, String? pageName, PersistedCameraState camera, UtilitiesState utilities, PersistedToolSelection selectedTool, bool navigatorEnabled, String navigatorPage, String currentLayer, String currentCollection, Set invisibleLayers, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, DateTime? updatedAt +}); + + +@override $PersistedCameraStateCopyWith<$Res> get camera;@override $UtilitiesStateCopyWith<$Res> get utilities;@override $PersistedToolSelectionCopyWith<$Res> get selectedTool; + +} +/// @nodoc +class __$PersistedDocumentStateCopyWithImpl<$Res> + implements _$PersistedDocumentStateCopyWith<$Res> { + __$PersistedDocumentStateCopyWithImpl(this._self, this._then); + + final _PersistedDocumentState _self; + final $Res Function(_PersistedDocumentState) _then; + +/// Create a copy of PersistedDocumentState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? version = null,Object? pathKey = freezed,Object? contentHash = freezed,Object? pageName = freezed,Object? camera = null,Object? utilities = null,Object? selectedTool = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? currentLayer = null,Object? currentCollection = null,Object? invisibleLayers = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? updatedAt = freezed,}) { + return _then(_PersistedDocumentState( +version: null == version ? _self.version : version // ignore: cast_nullable_to_non_nullable +as int,pathKey: freezed == pathKey ? _self.pathKey : pathKey // ignore: cast_nullable_to_non_nullable +as String?,contentHash: freezed == contentHash ? _self.contentHash : contentHash // ignore: cast_nullable_to_non_nullable +as String?,pageName: freezed == pageName ? _self.pageName : pageName // ignore: cast_nullable_to_non_nullable +as String?,camera: null == camera ? _self.camera : camera // ignore: cast_nullable_to_non_nullable +as PersistedCameraState,utilities: null == utilities ? _self.utilities : utilities // ignore: cast_nullable_to_non_nullable +as UtilitiesState,selectedTool: null == selectedTool ? _self.selectedTool : selectedTool // ignore: cast_nullable_to_non_nullable +as PersistedToolSelection,navigatorEnabled: null == navigatorEnabled ? _self.navigatorEnabled : navigatorEnabled // ignore: cast_nullable_to_non_nullable +as bool,navigatorPage: null == navigatorPage ? _self.navigatorPage : navigatorPage // ignore: cast_nullable_to_non_nullable +as String,currentLayer: null == currentLayer ? _self.currentLayer : currentLayer // ignore: cast_nullable_to_non_nullable +as String,currentCollection: null == currentCollection ? _self.currentCollection : currentCollection // ignore: cast_nullable_to_non_nullable +as String,invisibleLayers: null == invisibleLayers ? _self._invisibleLayers : invisibleLayers // ignore: cast_nullable_to_non_nullable +as Set,areaNavigatorCreate: null == areaNavigatorCreate ? _self.areaNavigatorCreate : areaNavigatorCreate // ignore: cast_nullable_to_non_nullable +as bool,areaNavigatorExact: null == areaNavigatorExact ? _self.areaNavigatorExact : areaNavigatorExact // ignore: cast_nullable_to_non_nullable +as bool,areaNavigatorAsk: null == areaNavigatorAsk ? _self.areaNavigatorAsk : areaNavigatorAsk // ignore: cast_nullable_to_non_nullable +as bool,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable +as DateTime?, + )); +} + +/// Create a copy of PersistedDocumentState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$PersistedCameraStateCopyWith<$Res> get camera { + + return $PersistedCameraStateCopyWith<$Res>(_self.camera, (value) { + return _then(_self.copyWith(camera: value)); + }); +}/// Create a copy of PersistedDocumentState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$UtilitiesStateCopyWith<$Res> get utilities { + + return $UtilitiesStateCopyWith<$Res>(_self.utilities, (value) { + return _then(_self.copyWith(utilities: value)); + }); +}/// Create a copy of PersistedDocumentState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$PersistedToolSelectionCopyWith<$Res> get selectedTool { + + return $PersistedToolSelectionCopyWith<$Res>(_self.selectedTool, (value) { + return _then(_self.copyWith(selectedTool: value)); + }); +} +} + +// dart format on diff --git a/app/lib/models/persisted_document_state.g.dart b/app/lib/models/persisted_document_state.g.dart new file mode 100644 index 000000000000..ffe76bbe3c72 --- /dev/null +++ b/app/lib/models/persisted_document_state.g.dart @@ -0,0 +1,95 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'persisted_document_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_PersistedToolSelection _$PersistedToolSelectionFromJson(Map json) => + _PersistedToolSelection( + toolId: json['toolId'] as String?, + toolIndex: (json['toolIndex'] as num?)?.toInt(), + ); + +Map _$PersistedToolSelectionToJson( + _PersistedToolSelection instance, +) => { + 'toolId': instance.toolId, + 'toolIndex': instance.toolIndex, +}; + +_PersistedCameraState _$PersistedCameraStateFromJson(Map json) => + _PersistedCameraState( + positionX: (json['positionX'] as num?)?.toDouble() ?? 0, + positionY: (json['positionY'] as num?)?.toDouble() ?? 0, + zoom: (json['zoom'] as num?)?.toDouble() ?? 1, + ); + +Map _$PersistedCameraStateToJson( + _PersistedCameraState instance, +) => { + 'positionX': instance.positionX, + 'positionY': instance.positionY, + 'zoom': instance.zoom, +}; + +_PersistedDocumentState _$PersistedDocumentStateFromJson(Map json) => + _PersistedDocumentState( + version: + (json['version'] as num?)?.toInt() ?? kPersistedDocumentStateVersion, + pathKey: json['pathKey'] as String?, + contentHash: json['contentHash'] as String?, + pageName: json['pageName'] as String?, + camera: json['camera'] == null + ? const PersistedCameraState() + : PersistedCameraState.fromJson( + Map.from(json['camera'] as Map), + ), + utilities: json['utilities'] == null + ? const UtilitiesState() + : UtilitiesState.fromJson( + Map.from(json['utilities'] as Map), + ), + selectedTool: json['selectedTool'] == null + ? const PersistedToolSelection() + : PersistedToolSelection.fromJson( + Map.from(json['selectedTool'] as Map), + ), + navigatorEnabled: json['navigatorEnabled'] as bool? ?? false, + navigatorPage: json['navigatorPage'] as String? ?? 'waypoints', + currentLayer: json['currentLayer'] as String? ?? '', + currentCollection: json['currentCollection'] as String? ?? '', + invisibleLayers: + (json['invisibleLayers'] as List?) + ?.map((e) => e as String) + .toSet() ?? + const {}, + areaNavigatorCreate: json['areaNavigatorCreate'] as bool? ?? true, + areaNavigatorExact: json['areaNavigatorExact'] as bool? ?? true, + areaNavigatorAsk: json['areaNavigatorAsk'] as bool? ?? false, + updatedAt: json['updatedAt'] == null + ? null + : DateTime.parse(json['updatedAt'] as String), + ); + +Map _$PersistedDocumentStateToJson( + _PersistedDocumentState instance, +) => { + 'version': instance.version, + 'pathKey': instance.pathKey, + 'contentHash': instance.contentHash, + 'pageName': instance.pageName, + 'camera': instance.camera.toJson(), + 'utilities': instance.utilities.toJson(), + 'selectedTool': instance.selectedTool.toJson(), + 'navigatorEnabled': instance.navigatorEnabled, + 'navigatorPage': instance.navigatorPage, + 'currentLayer': instance.currentLayer, + 'currentCollection': instance.currentCollection, + 'invisibleLayers': instance.invisibleLayers.toList(), + 'areaNavigatorCreate': instance.areaNavigatorCreate, + 'areaNavigatorExact': instance.areaNavigatorExact, + 'areaNavigatorAsk': instance.areaNavigatorAsk, + 'updatedAt': instance.updatedAt?.toIso8601String(), +}; diff --git a/app/lib/models/viewport.dart b/app/lib/models/viewport.dart index e4cbfd9c5b8a..628949a85e54 100644 --- a/app/lib/models/viewport.dart +++ b/app/lib/models/viewport.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'dart:ui' as ui; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/renderers/renderer.dart'; diff --git a/app/lib/renderers/elements/image.dart b/app/lib/renderers/elements/image.dart index e9ff23fca404..170274d02e50 100644 --- a/app/lib/renderers/elements/image.dart +++ b/app/lib/renderers/elements/image.dart @@ -89,7 +89,7 @@ class ImageRenderer extends Renderer { @override Future onVisible( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, DocumentLoaded blocState, CameraTransform renderTransform, ui.Size size, @@ -106,7 +106,7 @@ class ImageRenderer extends Renderer { @override void onHidden( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, DocumentLoaded blocState, CameraTransform renderTransform, ui.Size size, diff --git a/app/lib/renderers/elements/pdf.dart b/app/lib/renderers/elements/pdf.dart index 4d00acecef1b..4323c5fecc7a 100644 --- a/app/lib/renderers/elements/pdf.dart +++ b/app/lib/renderers/elements/pdf.dart @@ -89,7 +89,7 @@ class PdfRenderer extends Renderer { @override void onHidden( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, DocumentLoaded blocState, CameraTransform renderTransform, ui.Size size, @@ -104,7 +104,7 @@ class PdfRenderer extends Renderer { @override Future onVisible( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, DocumentLoaded blocState, CameraTransform renderTransform, ui.Size size, @@ -118,7 +118,7 @@ class PdfRenderer extends Renderer { @override Future updateView( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, DocumentLoaded blocState, CameraTransform renderTransform, ui.Size size, diff --git a/app/lib/renderers/elements/pen.dart b/app/lib/renderers/elements/pen.dart index bef67fe63520..dd48eec5e9d3 100644 --- a/app/lib/renderers/elements/pen.dart +++ b/app/lib/renderers/elements/pen.dart @@ -148,7 +148,7 @@ class PenRenderer extends Renderer { @override void onHidden( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, DocumentLoaded blocState, CameraTransform renderTransform, ui.Size size, diff --git a/app/lib/renderers/elements/polygon.dart b/app/lib/renderers/elements/polygon.dart index b3f5dc3200e9..f6b742495d72 100644 --- a/app/lib/renderers/elements/polygon.dart +++ b/app/lib/renderers/elements/polygon.dart @@ -164,11 +164,11 @@ class PolygonRenderer extends Renderer { ContextMenuItem? getContextMenuItem(DocumentBloc bloc, BuildContext context) { return ContextMenuItem( onPressed: () async { - bloc.currentIndexCubit.fetchHandler()?.clearSelection( + bloc.editorController.fetchHandler()?.clearSelection( bloc, ); final polygon = - await bloc.currentIndexCubit.changeTemporaryHandler( + await bloc.editorController.changeTemporaryHandler( context, PolygonTool(property: element.property), bloc: bloc, diff --git a/app/lib/renderers/elements/text.dart b/app/lib/renderers/elements/text.dart index 312537738269..fa9323277ac2 100644 --- a/app/lib/renderers/elements/text.dart +++ b/app/lib/renderers/elements/text.dart @@ -285,7 +285,7 @@ abstract class GenericTextRenderer extends Renderer { @override Future onVisible( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, DocumentLoaded blocState, CameraTransform renderTransform, ui.Size size, @@ -300,7 +300,7 @@ abstract class GenericTextRenderer extends Renderer { @override Future updateView( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, DocumentLoaded blocState, CameraTransform renderTransform, ui.Size size, @@ -315,7 +315,7 @@ abstract class GenericTextRenderer extends Renderer { @override void onHidden( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, DocumentLoaded blocState, CameraTransform renderTransform, ui.Size size, diff --git a/app/lib/renderers/renderer.dart b/app/lib/renderers/renderer.dart index ec92168ba28c..f906f90f51bc 100644 --- a/app/lib/renderers/renderer.dart +++ b/app/lib/renderers/renderer.dart @@ -5,7 +5,7 @@ import 'dart:ui' as ui; import 'package:butterfly/api/image.dart'; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/handlers/handler.dart'; import 'package:butterfly/helpers/element.dart'; import 'package:butterfly/helpers/markdown/latex.dart'; @@ -667,21 +667,21 @@ abstract class Renderer { ) => null; FutureOr onVisible( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, DocumentLoaded blocState, CameraTransform renderTransform, ui.Size size, ) {} FutureOr onHidden( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, DocumentLoaded blocState, CameraTransform renderTransform, ui.Size size, ) {} FutureOr updateView( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, DocumentLoaded blocState, CameraTransform renderTransform, ui.Size size, diff --git a/app/lib/selections/document.dart b/app/lib/selections/document.dart index 8e2b73642ea7..913b423d0f0d 100644 --- a/app/lib/selections/document.dart +++ b/app/lib/selections/document.dart @@ -1,7 +1,7 @@ part of 'selection.dart'; -class DocumentSelection extends Selection { - DocumentSelection(CurrentIndexCubit cubit) : super([cubit]); +class DocumentSelection extends Selection { + DocumentSelection(EditorController cubit) : super([cubit]); @override IconGetter get icon => PhosphorIcons.wrench; @@ -16,14 +16,16 @@ class DocumentSelection extends Selection { @override List buildProperties(BuildContext context) { final cubit = selected.first; - final currentIndex = cubit.state; + final viewState = cubit.viewCubit.state; return [ ...super.buildProperties(context), _UtilitiesView( - state: currentIndex.utilities, - option: currentIndex.viewOption, - onStateChanged: (state) => cubit.updateUtilities(utilities: state), - onToolChanged: (option) => cubit.updateUtilities(view: option), + state: viewState.utilities, + option: viewState.viewOption, + onStateChanged: (state) => + cubit.viewCubit.updateUtilities(utilities: state), + onToolChanged: (option) => + cubit.viewCubit.updateUtilities(view: option), ), ]; } @@ -155,10 +157,8 @@ class _UtilitiesViewState extends State<_UtilitiesView> ListTile( leading: const PhosphorIcon(PhosphorIconsLight.camera), onTap: () async { - final cubit = context - .read() - .currentIndexCubit; - final viewport = cubit.state.cameraViewport; + final cubit = context.read().editorController; + final viewport = cubit.rendererCubit.state.cameraViewport; final rect = viewport.toRealRect(); final targetAspectRatio = kThumbnailWidth / kThumbnailHeight; @@ -408,11 +408,12 @@ class _UtilitiesViewState extends State<_UtilitiesView> max: kMaxZoom * 100, onChangeEnd: (value) { final size = context - .read() + .read() + .rendererCubit .state .cameraViewport .toSize(); - context.read().size( + context.read().size( value / 100, Offset(size.width / 2, size.height / 2), ); diff --git a/app/lib/selections/selection.dart b/app/lib/selections/selection.dart index 3efa66fc3d94..e66f979d0b58 100644 --- a/app/lib/selections/selection.dart +++ b/app/lib/selections/selection.dart @@ -1,7 +1,7 @@ import 'dart:math'; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/dialogs/constraints.dart'; import 'package:butterfly/dialogs/texture.dart'; @@ -76,7 +76,7 @@ abstract class Selection { if (selected is Area) { return AreaSelection([selected]) as Selection; } - if (selected is CurrentIndexCubit) { + if (selected is EditorController) { return DocumentSelection(selected) as Selection; } throw UnsupportedError('Unsupported selection type: $T'); diff --git a/app/lib/services/export.dart b/app/lib/services/export.dart index b8fadc8e4861..5ea83c438aad 100644 --- a/app/lib/services/export.dart +++ b/app/lib/services/export.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/helpers/element.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter/material.dart'; @@ -19,7 +19,7 @@ class ExportService { ? (bloc?.state as DocumentLoadSuccess) : null; NoteData? _getDocument() => _getState()?.data; - CurrentIndexCubit? get currentIndexCubit => bloc?.currentIndexCubit; + EditorController? get editorController => bloc?.editorController; bool isExportable(PadElement element) => getExportInfo(element) != null; diff --git a/app/lib/services/import.dart b/app/lib/services/import.dart index 86bdef20717d..1b0a90b35f78 100644 --- a/app/lib/services/import.dart +++ b/app/lib/services/import.dart @@ -5,7 +5,7 @@ import 'dart:ui' as ui; import 'package:archive/archive.dart'; import 'package:butterfly/api/file_system.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/services/asset.dart'; import 'package:butterfly_api/butterfly_text.dart' as text; @@ -159,7 +159,7 @@ class ImportResult { if (choosePosition && state != null && (elements.isNotEmpty || areas.isNotEmpty)) { - service.currentIndexCubit?.changeTemporaryHandler( + service.editorController?.changeTemporaryHandler( context, ImportTool(elements: elements, areas: areas, assets: assets), bloc: bloc!, @@ -207,7 +207,7 @@ class ImportService { DocumentLoadSuccess? _getState() => bloc?.state is DocumentLoadSuccess ? (bloc?.state as DocumentLoadSuccess) : null; - CurrentIndexCubit? get currentIndexCubit => bloc?.currentIndexCubit; + EditorController? get editorController => bloc?.editorController; SettingsCubit getSettingsCubit() => context.read(); ButterflySettings getSettings() => getSettingsCubit().state; ButterflyFileSystem getFileSystem() => context.read(); @@ -237,7 +237,7 @@ class ImportService { Object? data, NoteData? document, }) async { - final location = bloc?.currentIndexCubit.state.location; + final location = bloc?.editorController.saveCubit.state.location; Uint8List? bytes; final fs = getDocumentSystem(); if (data is Uint8List) { @@ -607,13 +607,13 @@ class ImportService { image.dispose(); final settingsScale = getSettingsCubit().state.imageScale; ElementConstraints? constraints; - if (position == null && currentIndexCubit != null && settingsScale > 0) { + if (position == null && editorController != null && settingsScale > 0) { final scale = min( (screen.width * settingsScale) / width, (screen.height * settingsScale) / height, ) / - currentIndexCubit!.state.cameraViewport.scale; + editorController!.rendererCubit.state.cameraViewport.scale; constraints = ElementConstraints.scaled(scaleX: scale, scaleY: scale); } return ImportResult( @@ -920,14 +920,14 @@ class ImportService { final settingsScale = getSettingsCubit().state.imageScale; ElementConstraints? constraints; if (position == null && - currentIndexCubit != null && + editorController != null && settingsScale > 0) { final scale = min( (screen.width * settingsScale) / width, (screen.height * settingsScale) / height, ) / - currentIndexCubit!.state.cameraViewport.scale; + editorController!.rendererCubit.state.cameraViewport.scale; constraints = ElementConstraints.scaled( scaleX: scale, scaleY: scale, @@ -1186,19 +1186,13 @@ class ImportService { final bloc = this.bloc; final state = bloc?.state; if (state is! DocumentLoadSuccess) return; - final fileType = bloc?.currentIndexCubit.state.location.fileType; - final currentIndexCubit = bloc!.currentIndexCubit; - final viewport = currentIndexCubit.state.cameraViewport; + final fileType = bloc?.editorController.saveCubit.state.location.fileType; + final editorController = bloc!.editorController; + final viewport = editorController.rendererCubit.state.cameraViewport; switch (fileType) { case AssetFileType.note: case AssetFileType.textNote: - exportData( - context, - await state.saveData( - null, - context.read().state.viewOption, - ), - ); + exportData(context, await state.saveData()); break; case AssetFileType.image: return showDialog( diff --git a/app/lib/services/network.dart b/app/lib/services/network.dart index 90047167d936..b73d41e0f5e0 100644 --- a/app/lib/services/network.dart +++ b/app/lib/services/network.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:math'; import 'package:butterfly/bloc/document_bloc.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:flutter/foundation.dart'; @@ -326,7 +327,7 @@ class NetworkingService extends Cubit { final users = Map.from(_users.value) ..[message.channel] = user; _emitUsers(users); - _bloc?.currentIndexCubit.updateNetworkingState(_bloc!, users); + _bloc?.editorController.updateNetworkingState(_bloc!, users); }), ); rpc.getNamedFunction(NetworkEvent.undo)?.read.listen((_) { diff --git a/app/lib/settings/data.dart b/app/lib/settings/data.dart index 885d19557255..851d05a871f0 100644 --- a/app/lib/settings/data.dart +++ b/app/lib/settings/data.dart @@ -5,7 +5,7 @@ import 'package:archive/archive.dart'; import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/api/save.dart'; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/dialogs/template.dart'; @@ -153,7 +153,7 @@ class _DataSettingsPageState extends State { ); return DocumentBloc.placeholder( context.read(), - CurrentIndexCubit( + EditorController( context.read(), transformCubit, CameraViewport.unbaked(), diff --git a/app/lib/view_painter.dart b/app/lib/view_painter.dart index 7a14b39d7a76..a785260bae64 100644 --- a/app/lib/view_painter.dart +++ b/app/lib/view_painter.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/helpers/rect.dart'; import 'package:butterfly/models/viewport.dart'; diff --git a/app/lib/views/app_bar.dart b/app/lib/views/app_bar.dart index 27159eab9910..fa34c2be58a8 100644 --- a/app/lib/views/app_bar.dart +++ b/app/lib/views/app_bar.dart @@ -6,7 +6,7 @@ import 'package:butterfly/actions/settings.dart'; import 'package:butterfly/actions/svg_export.dart'; import 'package:butterfly/api/open.dart'; import 'package:butterfly/api/save.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/dialogs/collaboration/dialog.dart'; import 'package:butterfly/main.dart'; @@ -138,7 +138,7 @@ class _AppBarTitleState extends State<_AppBarTitle> { @override Widget build(BuildContext context) { final bloc = context.read(); - return BlocBuilder( + return BlocBuilder( buildWhen: (previous, current) => previous.location != current.location || previous.absolute != current.absolute || @@ -241,17 +241,17 @@ class _AppBarTitleState extends State<_AppBarTitle> { Area? area, String? areaName, DocumentBloc bloc, - CurrentIndex currentIndex, + DocumentSaveState currentIndex, BuildContext context, ButterflySettings settings, ) { - final cubit = context.read(); + final cubit = context.read(); return Row( textDirection: TextDirection.ltr, children: [ Flexible( child: StreamBuilder( - stream: context.read().networkingService.stream, + stream: context.read().networkingService.stream, builder: (context, snapshot) { return StatefulBuilder( builder: (context, setState) { @@ -274,8 +274,8 @@ class _AppBarTitleState extends State<_AppBarTitle> { ? _nameController.text : _areaController.text; if (area == null || areaName == null) { - final cubit = context.read(); - final location = cubit.state.location; + final cubit = context.read(); + final location = cubit.saveCubit.state.location; if (state is DocumentLoadSuccess && currentIndex.isCreating) { final newLocation = location.copyWith( @@ -487,7 +487,7 @@ class MainPopupMenu extends StatelessWidget { @override Widget build(BuildContext context) { - final cubit = context.read(); + final cubit = context.read(); final windowCubit = context.read(); return BlocBuilder( buildWhen: (previous, current) => @@ -498,401 +498,424 @@ class MainPopupMenu extends StatelessWidget { buildWhen: (previous, current) => previous.fullScreen != current.fullScreen, builder: (context, windowState) { - return BlocBuilder( + return BlocBuilder( buildWhen: (previous, current) => previous.embedding != current.embedding || - previous.hideUi != current.hideUi || previous.saved != current.saved, - builder: (context, state) { - final size = MediaQuery.sizeOf(context); - final navigatorRailEnabled = - settings.navigationRail || state.embedding != null; - final showNavigatorDialog = - MediaQuery.sizeOf(context).width < - LeapBreakpoints.expanded || - !navigatorRailEnabled || - windowState.fullScreen || - state.hideUi != HideState.visible; - return MenuAnchor( - menuChildren: [ - if (showNavigatorDialog) - ...NavigatorPage.values.map( - (e) => MenuItemButton( - leadingIcon: PhosphorIcon( - e.icon(PhosphorIconsStyle.light), - ), - child: Text(e.getLocalizedName(context)), - onPressed: () { - cubit.setNavigatorPage(e); - final bloc = context.read(); - final transformCubit = context - .read(); - showDialog( - context: context, - builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: bloc), - BlocProvider.value(value: cubit), - BlocProvider.value(value: transformCubit), - ], - child: DocumentNavigator(asDialog: true), + builder: (context, saveState) { + return BlocBuilder( + buildWhen: (previous, current) => + previous.hideUi != current.hideUi, + builder: (context, inputState) { + final size = MediaQuery.sizeOf(context); + final navigatorRailEnabled = + settings.navigationRail || saveState.embedding != null; + final showNavigatorDialog = + MediaQuery.sizeOf(context).width < + LeapBreakpoints.expanded || + !navigatorRailEnabled || + windowState.fullScreen || + inputState.hideUi != HideState.visible; + return MenuAnchor( + menuChildren: [ + if (showNavigatorDialog) + ...NavigatorPage.values.map( + (e) => MenuItemButton( + leadingIcon: PhosphorIcon( + e.icon(PhosphorIconsStyle.light), ), - ); - }, - ), - ), - if (showNavigatorDialog) const Divider(), - if (state.embedding == null) ...[ - MenuItemButton( - leadingIcon: const PhosphorIcon( - PhosphorIconsLight.house, - ), - child: Text(AppLocalizations.of(context).home), - onPressed: () async { - final router = GoRouter.of(context); - final bloc = context.read(); - await bloc.save(); - router.go('/'); - }, - ), - MenuItemButton( - leadingIcon: const PhosphorIcon( - PhosphorIconsLight.image, - ), - shortcut: const SingleActivator( - LogicalKeyboardKey.keyB, - control: true, - ), - onPressed: () { - Actions.maybeInvoke( - context, - BackgroundIntent(), - ); - }, - child: Text(AppLocalizations.of(context).background), - ), - SubmenuButton( - menuChildren: [ + child: Text(e.getLocalizedName(context)), + onPressed: () { + context.read().setNavigator( + page: e, + ); + final bloc = context.read(); + final transformCubit = context + .read(); + showDialog( + context: context, + builder: (context) => MultiBlocProvider( + providers: [ + BlocProvider.value(value: bloc), + BlocProvider.value(value: transformCubit), + ], + child: RepositoryProvider.value( + value: cubit, + child: DocumentNavigator(asDialog: true), + ), + ), + ); + }, + ), + ), + if (showNavigatorDialog) const Divider(), + if (saveState.embedding == null) ...[ MenuItemButton( leadingIcon: const PhosphorIcon( - PhosphorIconsLight.archive, + PhosphorIconsLight.house, + ), + child: Text(AppLocalizations.of(context).home), + onPressed: () async { + final router = GoRouter.of(context); + final bloc = context.read(); + await bloc.save(); + router.go('/'); + }, + ), + MenuItemButton( + leadingIcon: const PhosphorIcon( + PhosphorIconsLight.image, ), shortcut: const SingleActivator( - LogicalKeyboardKey.keyE, + LogicalKeyboardKey.keyB, control: true, ), - onPressed: () async { - Actions.maybeInvoke( + onPressed: () { + Actions.maybeInvoke( context, - ExportIntent(), + BackgroundIntent(), ); }, child: Text( - AppLocalizations.of(context).packagedFile, + AppLocalizations.of(context).background, ), ), + SubmenuButton( + menuChildren: [ + MenuItemButton( + leadingIcon: const PhosphorIcon( + PhosphorIconsLight.archive, + ), + shortcut: const SingleActivator( + LogicalKeyboardKey.keyE, + control: true, + ), + onPressed: () async { + Actions.maybeInvoke( + context, + ExportIntent(), + ); + }, + child: Text( + AppLocalizations.of(context).packagedFile, + ), + ), + MenuItemButton( + leadingIcon: const PhosphorIcon( + PhosphorIconsLight.file, + textDirection: TextDirection.ltr, + ), + shortcut: const SingleActivator( + LogicalKeyboardKey.keyE, + control: true, + shift: true, + ), + onPressed: () async { + Actions.maybeInvoke( + context, + ExportIntent(isText: true), + ); + }, + child: Text( + AppLocalizations.of(context).rawFile, + ), + ), + MenuItemButton( + leadingIcon: const PhosphorIcon( + PhosphorIconsLight.fileSvg, + textDirection: TextDirection.ltr, + ), + shortcut: const SingleActivator( + LogicalKeyboardKey.keyE, + alt: true, + control: true, + ), + onPressed: () async { + Actions.maybeInvoke( + context, + SvgExportIntent(), + ); + }, + child: Text(AppLocalizations.of(context).svg), + ), + MenuItemButton( + leadingIcon: const PhosphorIcon( + PhosphorIconsLight.fileImage, + textDirection: TextDirection.ltr, + ), + shortcut: const SingleActivator( + LogicalKeyboardKey.keyE, + alt: true, + control: true, + shift: true, + ), + onPressed: () { + Actions.maybeInvoke( + context, + ImageExportIntent(), + ); + }, + child: Text(AppLocalizations.of(context).image), + ), + MenuItemButton( + leadingIcon: const PhosphorIcon( + PhosphorIconsLight.filePdf, + textDirection: TextDirection.ltr, + ), + shortcut: const SingleActivator( + LogicalKeyboardKey.keyP, + shift: true, + control: true, + ), + onPressed: () { + Actions.maybeInvoke( + context, + PdfExportIntent(), + ); + }, + child: Text(AppLocalizations.of(context).pdf), + ), + MenuItemButton( + leadingIcon: const PhosphorIcon( + PhosphorIconsLight.notebook, + ), + onPressed: () async { + final bloc = context.read(); + final state = bloc.state; + if (state is! DocumentLoadSuccess) return; + final data = await state.saveData(); + if (!context.mounted) return; + exportXopp( + context, + xoppExporter(data), + fileName: state.metadata.name, + ); + }, + child: const Text('Xournal++'), + ), + ], + leadingIcon: const PhosphorIcon( + PhosphorIconsLight.paperPlaneRight, + ), + child: Text(AppLocalizations.of(context).export), + ), MenuItemButton( leadingIcon: const PhosphorIcon( - PhosphorIconsLight.file, - textDirection: TextDirection.ltr, + PhosphorIconsLight.package, ), shortcut: const SingleActivator( - LogicalKeyboardKey.keyE, + LogicalKeyboardKey.keyP, control: true, - shift: true, + alt: true, ), - onPressed: () async { - Actions.maybeInvoke( + onPressed: () { + Actions.maybeInvoke( context, - ExportIntent(isText: true), + PacksIntent(), ); }, - child: Text(AppLocalizations.of(context).rawFile), + child: Text(AppLocalizations.of(context).packs), ), + const Divider(), MenuItemButton( leadingIcon: const PhosphorIcon( - PhosphorIconsLight.fileSvg, + PhosphorIconsLight.filePlus, textDirection: TextDirection.ltr, ), shortcut: const SingleActivator( - LogicalKeyboardKey.keyE, - alt: true, + LogicalKeyboardKey.keyN, control: true, ), - onPressed: () async { - Actions.maybeInvoke( + onPressed: () { + Actions.maybeInvoke( context, - SvgExportIntent(), + NewIntent(), ); }, - child: Text(AppLocalizations.of(context).svg), + child: Text( + AppLocalizations.of(context).newContent, + ), ), MenuItemButton( leadingIcon: const PhosphorIcon( - PhosphorIconsLight.fileImage, + PhosphorIconsLight.file, textDirection: TextDirection.ltr, ), shortcut: const SingleActivator( - LogicalKeyboardKey.keyE, - alt: true, - control: true, + LogicalKeyboardKey.keyN, shift: true, + control: true, ), onPressed: () { - Actions.maybeInvoke( + Actions.maybeInvoke( context, - ImageExportIntent(), + NewIntent(fromTemplate: true), ); }, - child: Text(AppLocalizations.of(context).image), + child: Text(AppLocalizations.of(context).templates), + ), + SubmenuButton( + menuChildren: settings.history + .map( + (e) => MenuItemButton( + child: Text(e.identifier), + onPressed: () => openFile(context, true, e), + ), + ) + .toList(), + leadingIcon: const PhosphorIcon( + PhosphorIconsLight.clock, + ), + child: Text( + AppLocalizations.of(context).recentFiles, + ), ), + ], + if (saveState.embedding == null) ...[ MenuItemButton( leadingIcon: const PhosphorIcon( - PhosphorIconsLight.filePdf, - textDirection: TextDirection.ltr, + PhosphorIconsLight.gear, ), shortcut: const SingleActivator( - LogicalKeyboardKey.keyP, - shift: true, + LogicalKeyboardKey.keyS, + alt: true, control: true, ), + onPressed: () => openSettings(context), + child: Text(AppLocalizations.of(context).settings), + ), + MenuItemButton( + leadingIcon: const PhosphorIcon( + PhosphorIconsLight.eyeSlash, + ), + shortcut: const SingleActivator( + LogicalKeyboardKey.f12, + ), onPressed: () { - Actions.maybeInvoke( - context, - PdfExportIntent(), + context + .read() + .enterTouchHideUI(); + }, + child: Text(AppLocalizations.of(context).hideUI), + ), + BlocBuilder( + buildWhen: (previous, current) => + previous.fullScreen != current.fullScreen, + builder: (context, windowState) => MenuItemButton( + leadingIcon: windowState.fullScreen + ? const PhosphorIcon( + PhosphorIconsLight.arrowsIn, + ) + : const PhosphorIcon( + PhosphorIconsLight.arrowsOut, + ), + shortcut: const SingleActivator( + LogicalKeyboardKey.f11, + ), + onPressed: () async { + windowCubit.toggleFullScreen(); + }, + child: Text( + LeapLocalizations.of(context).fullScreen, + ), + ), + ), + ], + if (saveState.embedding == null && + settings.hasFlag('collaboration')) + BlocBuilder( + bloc: context + .read() + .networkingService, + builder: (_, state) { + final isOpen = state?.connection.isOpen ?? false; + return MenuItemButton( + leadingIcon: isOpen + ? Icon( + PhosphorIconsFill.users, + color: ColorScheme.of(context).primary, + ) + : Icon(PhosphorIconsLight.users), + onPressed: () => + showCollaborationDialog(context), + child: Text( + AppLocalizations.of(context).collaboration, + style: TextStyle( + color: isOpen + ? ColorScheme.of(context).primary + : null, + ), + ), ); }, - child: Text(AppLocalizations.of(context).pdf), ), + if (saveState.embedding?.onOpen != null) ...[ MenuItemButton( leadingIcon: const PhosphorIcon( - PhosphorIconsLight.notebook, + PhosphorIconsLight.folder, ), + onPressed: saveState.embedding?.onOpen, + child: Text(AppLocalizations.of(context).open), + ), + ], + if (saveState.embedding != null) ...[ + MenuItemButton( + leadingIcon: const PhosphorIcon( + PhosphorIconsLight.door, + ), + child: Text(AppLocalizations.of(context).exit), onPressed: () async { + final embedding = saveState.embedding!; + if (embedding.isInternal) { + embedding.onExit?.call(); + return; + } final bloc = context.read(); - final state = bloc.state; - if (state is! DocumentLoadSuccess) return; - final data = await state.saveData( - null, - bloc.currentIndexCubit.state.viewOption, - ); - if (!context.mounted) return; - exportXopp( - context, - xoppExporter(data), - fileName: state.metadata.name, + final documentState = bloc.state; + if (documentState is! DocumentLoaded) return; + sendEmbedMessage( + 'exit', + await documentState.saveBytes(), ); }, - child: const Text('Xournal++'), ), ], - leadingIcon: const PhosphorIcon( - PhosphorIconsLight.paperPlaneRight, - ), - child: Text(AppLocalizations.of(context).export), - ), - MenuItemButton( - leadingIcon: const PhosphorIcon( - PhosphorIconsLight.package, - ), - shortcut: const SingleActivator( - LogicalKeyboardKey.keyP, - control: true, - alt: true, - ), - onPressed: () { - Actions.maybeInvoke( - context, - PacksIntent(), - ); - }, - child: Text(AppLocalizations.of(context).packs), - ), - const Divider(), - MenuItemButton( - leadingIcon: const PhosphorIcon( - PhosphorIconsLight.filePlus, - textDirection: TextDirection.ltr, - ), - shortcut: const SingleActivator( - LogicalKeyboardKey.keyN, - control: true, - ), - onPressed: () { - Actions.maybeInvoke(context, NewIntent()); - }, - child: Text(AppLocalizations.of(context).newContent), - ), - MenuItemButton( - leadingIcon: const PhosphorIcon( - PhosphorIconsLight.file, - textDirection: TextDirection.ltr, - ), - shortcut: const SingleActivator( - LogicalKeyboardKey.keyN, - shift: true, - control: true, - ), - onPressed: () { - Actions.maybeInvoke( - context, - NewIntent(fromTemplate: true), - ); - }, - child: Text(AppLocalizations.of(context).templates), - ), - SubmenuButton( - menuChildren: settings.history - .map( - (e) => MenuItemButton( - child: Text(e.identifier), - onPressed: () => openFile(context, true, e), - ), - ) - .toList(), - leadingIcon: const PhosphorIcon( - PhosphorIconsLight.clock, - ), - child: Text(AppLocalizations.of(context).recentFiles), - ), - ], - if (state.embedding == null) ...[ - MenuItemButton( - leadingIcon: const PhosphorIcon( - PhosphorIconsLight.gear, - ), - shortcut: const SingleActivator( - LogicalKeyboardKey.keyS, - alt: true, - control: true, - ), - onPressed: () => openSettings(context), - child: Text(AppLocalizations.of(context).settings), - ), - MenuItemButton( - leadingIcon: const PhosphorIcon( - PhosphorIconsLight.eyeSlash, - ), - shortcut: const SingleActivator(LogicalKeyboardKey.f12), - onPressed: () { - context.read().enterTouchHideUI(); - }, - child: Text(AppLocalizations.of(context).hideUI), - ), - BlocBuilder( - buildWhen: (previous, current) => - previous.fullScreen != current.fullScreen, - builder: (context, windowState) => MenuItemButton( - leadingIcon: windowState.fullScreen - ? const PhosphorIcon(PhosphorIconsLight.arrowsIn) - : const PhosphorIcon( - PhosphorIconsLight.arrowsOut, - ), - shortcut: const SingleActivator( - LogicalKeyboardKey.f11, + ], + style: MenuStyle( + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: const BorderRadius.all( + Radius.circular(16), + ), ), - onPressed: () async { - windowCubit.toggleFullScreen(); - }, - child: Text(LeapLocalizations.of(context).fullScreen), ), - ), - ], - if (state.embedding == null && - settings.hasFlag('collaboration')) - BlocBuilder( - bloc: context - .read() - .networkingService, - builder: (_, state) { - final isOpen = state?.connection.isOpen ?? false; - return MenuItemButton( - leadingIcon: isOpen - ? Icon( - PhosphorIconsFill.users, - color: ColorScheme.of(context).primary, - ) - : Icon(PhosphorIconsLight.users), - onPressed: () => showCollaborationDialog(context), - child: Text( - AppLocalizations.of(context).collaboration, - style: TextStyle( - color: isOpen - ? ColorScheme.of(context).primary - : null, - ), - ), - ); - }, - ), - if (state.embedding?.onOpen != null) ...[ - MenuItemButton( - leadingIcon: const PhosphorIcon( - PhosphorIconsLight.folder, + maximumSize: WidgetStateProperty.all( + Size( + (size.width - 32).clamp(100.0, 300.0), + (size.height - 70 - (padding?.bottom ?? 0) * 1.5) + .clamp(100.0, double.infinity), + ), ), - onPressed: state.embedding?.onOpen, - child: Text(AppLocalizations.of(context).open), ), - ], - if (state.embedding != null) ...[ - MenuItemButton( - leadingIcon: const PhosphorIcon( - PhosphorIconsLight.door, - ), - child: Text(AppLocalizations.of(context).exit), - onPressed: () async { - final embedding = state.embedding!; - if (embedding.isInternal) { - embedding.onExit?.call(); - return; - } - final bloc = context.read(); - final documentState = bloc.state; - if (documentState is! DocumentLoaded) return; - sendEmbedMessage( - 'exit', - await documentState.saveBytes( - null, - bloc.currentIndexCubit.state.viewOption, + builder: (context, controller, child) => Align( + child: AspectRatio( + aspectRatio: 1, + child: IconButton( + icon: Image.asset(logoAsset), + style: IconButton.styleFrom( + backgroundColor: controller.isOpen + ? ColorScheme.of( + context, + ).surfaceContainerHighest + : null, ), - ); - }, - ), - ], - ], - style: MenuStyle( - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - borderRadius: const BorderRadius.all( - Radius.circular(16), - ), - ), - ), - maximumSize: WidgetStateProperty.all( - Size( - (size.width - 32).clamp(100.0, 300.0), - (size.height - 70 - (padding?.bottom ?? 0) * 1.5).clamp( - 100.0, - double.infinity, - ), - ), - ), - ), - builder: (context, controller, child) => Align( - child: AspectRatio( - aspectRatio: 1, - child: IconButton( - icon: Image.asset(logoAsset), - style: IconButton.styleFrom( - backgroundColor: controller.isOpen - ? ColorScheme.of(context).surfaceContainerHighest - : null, + tooltip: AppLocalizations.of(context).actions, + onPressed: controller.toggle, + ), ), - tooltip: AppLocalizations.of(context).actions, - onPressed: controller.toggle, ), - ), - ), + ); + }, ); }, ); diff --git a/app/lib/views/edit.dart b/app/lib/views/edit.dart index 34124add4f70..35ef1b9e28c0 100644 --- a/app/lib/views/edit.dart +++ b/app/lib/views/edit.dart @@ -1,5 +1,5 @@ import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/dialogs/import/add.dart'; import 'package:butterfly/services/import.dart'; import 'package:butterfly/visualizer/tool.dart'; @@ -83,26 +83,32 @@ class _EditToolbarState extends State { if (state is! DocumentLoadSuccess) return Container(); final tools = state.info.tools; - return BlocBuilder( + return BlocBuilder( buildWhen: (previous, current) => previous.index != current.index || previous.handler != current.handler || previous.toggleableHandlers != current.toggleableHandlers || previous.temporaryHandler != current.temporaryHandler || previous.selection != current.selection, - builder: (context, currentIndex) { - return Card( - elevation: 10, - child: _buildBody( - state, - currentIndex, - settings, - tools, - shortcuts, - size, + builder: (context, toolState) => + BlocBuilder( + buildWhen: (previous, current) => + previous.embedding != current.embedding, + builder: (context, saveState) { + return Card( + elevation: 10, + child: _buildBody( + state, + toolState, + saveState, + settings, + tools, + shortcuts, + size, + ), + ); + }, ), - ); - }, ); }, ); @@ -115,7 +121,8 @@ class _EditToolbarState extends State { PhosphorIcon(data, size: size * (6 / 16), color: color); Widget _buildBody( DocumentLoadSuccess state, - CurrentIndex currentIndex, + ToolRuntimeState currentIndex, + DocumentSaveState saveState, ButterflySettings settings, List tools, Set shortcuts, @@ -123,7 +130,7 @@ class _EditToolbarState extends State { ) { final fullSize = (size + 4) * settings.toolbarRows; final bloc = context.read(); - final cubit = context.read(); + final cubit = context.read(); final isMobile = widget.isMobile; final temp = currentIndex.temporaryHandler; final tempData = temp?.data; @@ -148,7 +155,7 @@ class _EditToolbarState extends State { scrollDirection: direction, shrinkWrap: true, slivers: [ - if (currentIndex.embedding?.editable ?? true) ...[ + if (saveState.embedding?.editable ?? true) ...[ if (temp != null && tempData != null) ...[ SliverToBoxAdapter( child: Padding( @@ -166,12 +173,12 @@ class _EditToolbarState extends State { false, icon: _buildIcon(icon, size), selectedIcon: _buildIcon(iconFilled, size), - onLongPressed: () => cubit.changeSelection(tempData), + onLongPressed: () => cubit.toolCubit.changeSelection(tempData), onPressed: () { if (_mouseState == _MouseState.multi) { - cubit.insertSelection(tempData, true); + cubit.toolCubit.insertSelection(tempData, true); } else { - cubit.changeSelection(tempData, true); + cubit.toolCubit.changeSelection(tempData, true); } }, ), @@ -224,10 +231,14 @@ class _EditToolbarState extends State { pageBuilder: (ctx, _, _) => MultiBlocProvider( providers: [ BlocProvider.value(value: bloc), - BlocProvider.value(value: cubit), ], - child: RepositoryProvider.value( - value: importService, + child: MultiRepositoryProvider( + providers: [ + RepositoryProvider.value(value: cubit), + RepositoryProvider.value( + value: importService, + ), + ], child: const AddDialog(), ), ), @@ -310,15 +321,15 @@ class _EditToolbarState extends State { onLongPressed: selected || highlighted ? null : () => context - .read() + .read() .insertSelection(tool, true), onDoubleTap: highlighted || selected ? () => context - .read() + .read() .insertSelection(tool, true) : null, onSecondaryPressed: () => context - .read() + .read() .changeSelection(tool), focussed: shortcuts.contains(InputMapping(i)), selected: @@ -361,9 +372,9 @@ class _EditToolbarState extends State { ), onPressed: () { if (_mouseState == _MouseState.multi) { - cubit.insertSelection(tool, true); + cubit.toolCubit.insertSelection(tool, true); } else if (!selected || temp != null) { - cubit.resetSelection(); + cubit.toolCubit.resetSelection(); cubit.changeTool( bloc, index: i, @@ -371,7 +382,7 @@ class _EditToolbarState extends State { context: context, ); } else { - cubit.changeSelection(tool, true); + cubit.toolCubit.changeSelection(tool, true); } }, ), @@ -383,7 +394,7 @@ class _EditToolbarState extends State { }, onReorder: (oldIndex, newIndex) { if (oldIndex == newIndex) { - context.read().insertSelection( + context.read().insertSelection( tools[newIndex], true, ); @@ -414,16 +425,16 @@ class _EditToolbarState extends State { ), isSelected: currentIndex.selection?.selected.any( - (element) => element is CurrentIndexCubit, + (element) => element is EditorController, ) ?? false, onPressed: () { - cubit.changeSelection(cubit); + cubit.toolCubit.changeSelection(cubit); }, ), - BlocBuilder( - builder: (context, currentIndex) { - final utilitiesState = currentIndex.utilities; + BlocBuilder( + builder: (context, viewState) { + final utilitiesState = viewState.utilities; Widget buildButton( bool selected, UtilitiesState Function() update, @@ -433,7 +444,7 @@ class _EditToolbarState extends State { value: selected, trailingIcon: PhosphorIcon(icon), onChanged: (value) => context - .read() + .read() .updateUtilities(utilities: update()), child: Text(title), ); diff --git a/app/lib/views/main.dart b/app/lib/views/main.dart index 148187981b02..b261bdd19c30 100644 --- a/app/lib/views/main.dart +++ b/app/lib/views/main.dart @@ -5,11 +5,13 @@ import 'package:butterfly/actions/shortcuts.dart'; import 'package:butterfly/api/close.dart'; import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; +import 'package:butterfly/cubits/editor_session.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/embed/embedding.dart'; import 'package:butterfly/models/defaults.dart'; +import 'package:butterfly/models/persisted_document_state.dart'; import 'package:butterfly/renderers/renderer.dart'; import 'package:butterfly/services/export.dart'; import 'package:butterfly/services/import.dart'; @@ -62,7 +64,8 @@ class ProjectPage extends StatefulWidget { class _ProjectDocumentRuntime { final DocumentBloc bloc; final TransformCubit transformCubit; - final CurrentIndexCubit currentIndexCubit; + final EditorSessionCubit? editorSessionCubit; + final EditorController editorController; final ImportService? importService; final ExportService? exportService; final Embedding? embedding; @@ -71,7 +74,8 @@ class _ProjectDocumentRuntime { _ProjectDocumentRuntime({ required this.bloc, required this.transformCubit, - required this.currentIndexCubit, + this.editorSessionCubit, + required this.editorController, this.importService, this.exportService, this.embedding, @@ -84,6 +88,9 @@ class _ProjectDocumentRuntime { if (!bloc.isClosed) { await bloc.close(); } + if (editorSessionCubit != null && !editorSessionCubit!.isClosed) { + await editorSessionCubit!.close(); + } } } @@ -192,6 +199,7 @@ class _ProjectPageState extends State { )?.name; NoteData? document; var data = widget.data; + Uint8List? loadedDocumentBytes; final uri = Uri.tryParse(widget.uri ?? ''); var type = widget.type.isEmpty ? (fileType ?? widget.type) : widget.type; if (widget.uri != null && uri != null) { @@ -216,6 +224,11 @@ class _ProjectPageState extends State { defaultDocument ??= DocumentDefaults.createDocument(name: name); bool failedToLoad = false; if (data != null) { + if (data is Uint8List) { + loadedDocumentBytes = data; + } else if (data is NoteFile) { + loadedDocumentBytes = data.data; + } document ??= await globalImportService .load(type: type, data: data, document: defaultDocument) .then((e) => e?.export()); @@ -234,6 +247,7 @@ class _ProjectPageState extends State { return; } if (asset is FileSystemFile) { + loadedDocumentBytes = asset.data?.data; final NoteData? noteData = await globalImportService .load( document: defaultDocument, @@ -252,6 +266,7 @@ class _ProjectPageState extends State { } else { final data = await documentSystem.loadAbsolute(location.path); if (data != null) { + loadedDocumentBytes = data; document = await globalImportService .load( document: defaultDocument, @@ -273,7 +288,7 @@ class _ProjectPageState extends State { } if (failedToLoad) { final transformCubit = TransformCubit(pixelRatio); - final currentIndexCubit = CurrentIndexCubit( + final editorController = EditorController( settingsCubit, transformCubit, CameraViewport.unbaked(), @@ -281,7 +296,7 @@ class _ProjectPageState extends State { ); final bloc = DocumentBloc.error( fileSystem, - currentIndexCubit, + editorController, windowCubit, AppLocalizations.of(context).errorWhileImportingContent, ); @@ -289,7 +304,7 @@ class _ProjectPageState extends State { _runtime = _ProjectDocumentRuntime( bloc: bloc, transformCubit: transformCubit, - currentIndexCubit: currentIndexCubit, + editorController: editorController, importService: ImportService(context, bloc: bloc), exportService: ExportService(context, bloc), embedding: embedding, @@ -317,9 +332,39 @@ class _ProjectPageState extends State { createdAt: DateTime.now(), ); } - final pageName = document.getPages(true).firstOrNull; + location ??= AssetLocation( + path: widget.location?.path ?? '', + remote: remote?.identifier ?? '', + ); + final pathKey = documentStatePathKeyOrNull(location); + final contentHash = loadedDocumentBytes == null + ? null + : documentStateContentHash(loadedDocumentBytes); + final documentStateSystem = fileSystem.buildDocumentStateSystem(remote); + final restoredSession = await EditorSessionCubit.load( + fileSystem: documentStateSystem, + contentHash: contentHash, + pathKey: pathKey, + allowContentHash: loadedDocumentBytes != null, + ); + final fallbackPageName = document.getPages(true).firstOrNull; + final restoredPageName = restoredSession?.pageName; + final pageName = + restoredPageName != null && + document.getPages(true).contains(restoredPageName) + ? restoredPageName + : fallbackPageName; final page = document.getPage(pageName ?? '') ?? DocumentDefaults.createPage(); + final initialSession = EditorSessionCubit.buildInitial( + restored: restoredSession, + document: document, + page: page, + fallbackPageName: pageName, + fallbackUtilities: settingsCubit.state.utilities, + pathKey: pathKey, + contentHash: contentHash, + ); final renderers = page.layers .expand( (layer) => layer.content.map( @@ -329,6 +374,20 @@ class _ProjectPageState extends State { .toList(); final assetService = AssetService(); final transformCubit = TransformCubit(pixelRatio); + transformCubit.teleport( + Offset( + initialSession.camera.positionX, + initialSession.camera.positionY, + ), + initialSession.camera.zoom, + ); + final editorSessionCubit = EditorSessionCubit( + fileSystem: documentStateSystem, + transformCubit: transformCubit, + initialState: initialSession, + pathKey: pathKey, + contentHash: contentHash, + ); pendingAssetService = assetService; pendingTransformCubit = transformCubit; pendingRenderers.addAll(renderers); @@ -353,16 +412,10 @@ class _ProjectPageState extends State { await disposePendingRuntime(); return; } - location ??= AssetLocation( - path: widget.location?.path ?? '', - remote: remote?.identifier ?? '', - ); - if (!isCurrentLoad()) { - await disposePendingRuntime(); - return; + if (restoredSession == null) { + transformCubit.teleportToWaypoint(page.getOriginWaypoint()); } - transformCubit.teleportToWaypoint(page.getOriginWaypoint()); - final currentIndexCubit = CurrentIndexCubit( + final editorController = EditorController( settingsCubit, transformCubit, CameraViewport.unbaked( @@ -373,17 +426,24 @@ class _ProjectPageState extends State { ), embedding: embedding, networkingService: networkingService, + editorSessionCubit: editorSessionCubit, absolute: absolute, ); final bloc = DocumentBloc( fileSystem, - currentIndexCubit, + editorController, windowCubit, document, location, assetService, page, pageName, + false, + initialSession.currentLayer.isEmpty + ? null + : initialSession.currentLayer, + initialSession.currentCollection, + initialSession.invisibleLayers, ); final isImportedDocument = documentOpened && !(location.fileType?.isNote() ?? false); @@ -395,7 +455,8 @@ class _ProjectPageState extends State { _runtime = _ProjectDocumentRuntime( bloc: bloc, transformCubit: transformCubit, - currentIndexCubit: currentIndexCubit, + editorSessionCubit: editorSessionCubit, + editorController: editorController, importService: ImportService(context, bloc: bloc), exportService: ExportService(context, bloc), embedding: embedding, @@ -417,7 +478,7 @@ class _ProjectPageState extends State { } await disposePendingRuntime(closeNetworkingService: false); final transformCubit = TransformCubit(pixelRatio); - final currentIndexCubit = CurrentIndexCubit( + final editorController = EditorController( settingsCubit, transformCubit, CameraViewport.unbaked(), @@ -425,7 +486,7 @@ class _ProjectPageState extends State { ); final bloc = DocumentBloc.error( fileSystem, - currentIndexCubit, + editorController, windowCubit, e.toString(), stackTrace, @@ -434,7 +495,8 @@ class _ProjectPageState extends State { _runtime = _ProjectDocumentRuntime( bloc: bloc, transformCubit: transformCubit, - currentIndexCubit: currentIndexCubit, + editorSessionCubit: null, + editorController: editorController, embedding: embedding, ); }); @@ -465,7 +527,13 @@ class _ProjectPageState extends State { providers: [ BlocProvider.value(value: runtime.bloc), BlocProvider.value(value: runtime.transformCubit), - BlocProvider.value(value: runtime.currentIndexCubit), + if (runtime.editorSessionCubit != null) + BlocProvider.value(value: runtime.editorSessionCubit!), + BlocProvider.value(value: runtime.editorController.rendererCubit), + BlocProvider.value(value: runtime.editorController.toolCubit), + BlocProvider.value(value: runtime.editorController.inputCubit), + BlocProvider.value(value: runtime.editorController.saveCubit), + BlocProvider.value(value: runtime.editorController.viewCubit), ], child: BlocBuilder( buildWhen: (previous, current) => @@ -479,6 +547,7 @@ class _ProjectPageState extends State { } return MultiRepositoryProvider( providers: [ + RepositoryProvider.value(value: runtime.editorController), RepositoryProvider.value(value: runtime.importService!), RepositoryProvider.value(value: runtime.exportService!), ], @@ -492,72 +561,78 @@ class _ProjectPageState extends State { }, child: BlocBuilder( builder: (context, windowState) => - BlocBuilder( + BlocBuilder( buildWhen: (previous, current) => - previous.hideUi != current.hideUi || - previous.embedding?.editable != - current.embedding?.editable || - previous.embedding?.isInternal != - current.embedding?.isInternal, - builder: (context, currentIndex) => - BlocBuilder( + previous.hideUi != current.hideUi, + builder: (context, inputState) => + BlocBuilder( buildWhen: (previous, current) => - previous.toolbarSize != current.toolbarSize || - previous.isInline != current.isInline, - builder: (context, settings) { - final actions = _buildActions(context); - - return ListenableBuilder( - listenable: keybinder, - builder: (context, child) { - final shortcuts = _buildShortcuts(); - return Actions( - actions: actions, - child: Shortcuts( - shortcuts: shortcuts, - child: child!, - ), - ); - }, - child: ClipRect( - child: Focus( - autofocus: true, - skipTraversal: true, - onFocusChange: (_) => false, - child: Scaffold( - appBar: - state is DocumentPresentationState || - windowState.fullScreen || - currentIndex.hideUi != - HideState.visible - ? null - : PadAppBar( - viewportKey: _viewportKey, - size: settings.toolbarSize, - searchController: - _searchController, - padding: padding, - direction: Directionality.of( - context, - ), - inView: - currentIndex - .embedding - ?.isInternal ?? - false, - showTools: - settings.isInline && - currentIndex - .embedding - ?.editable != - false, - ), - body: const _MainBody(), - ), - ), + previous.embedding?.editable != + current.embedding?.editable || + previous.embedding?.isInternal != + current.embedding?.isInternal, + builder: (context, saveState) => + BlocBuilder( + buildWhen: (previous, current) => + previous.toolbarSize != + current.toolbarSize || + previous.isInline != current.isInline, + builder: (context, settings) { + final actions = _buildActions(context); + + return ListenableBuilder( + listenable: keybinder, + builder: (context, child) { + final shortcuts = _buildShortcuts(); + return Actions( + actions: actions, + child: Shortcuts( + shortcuts: shortcuts, + child: child!, + ), + ); + }, + child: ClipRect( + child: Focus( + autofocus: true, + skipTraversal: true, + onFocusChange: (_) => false, + child: Scaffold( + appBar: + state is DocumentPresentationState || + windowState.fullScreen || + inputState.hideUi != + HideState.visible + ? null + : PadAppBar( + viewportKey: _viewportKey, + size: settings.toolbarSize, + searchController: + _searchController, + padding: padding, + direction: + Directionality.of( + context, + ), + inView: + saveState + .embedding + ?.isInternal ?? + false, + showTools: + settings.isInline && + saveState + .embedding + ?.editable != + false, + ), + body: const _MainBody(), + ), + ), + ), + ); + }, ), - ); - }, ), ), ), @@ -569,8 +644,8 @@ class _ProjectPageState extends State { } CloseRequest? _preventClose() { - final currentIndex = _runtime?.currentIndexCubit.state; - return currentIndex?.saved == SaveState.saved + final saveState = _runtime?.editorController.saveCubit.state; + return saveState?.saved == SaveState.saved ? null : CloseRequest( message: AppLocalizations.of(context).thereAreUnsavedChanges, @@ -582,7 +657,7 @@ class _ProjectPageState extends State { final bloc = _runtime?.bloc; if (bloc == null || bloc.isClosed) return false; await bloc.save(force: true); - return bloc.currentIndexCubit.state.saved == SaveState.saved; + return bloc.editorController.saveCubit.state.saved == SaveState.saved; } Map> _buildActions(BuildContext context) { @@ -656,38 +731,56 @@ class _MainBody extends StatelessWidget { (previous is DocumentLoadSuccess && current is! DocumentLoadSuccess) || (previous is! DocumentLoadSuccess && current is DocumentLoadSuccess), - builder: (context, state) => BlocBuilder( + builder: (context, state) => BlocBuilder( buildWhen: (previous, current) => previous.pinned != current.pinned || - previous.selection != current.selection || - previous.hideUi != current.hideUi, - builder: (context, currentIndex) => - BlocBuilder( - builder: (context, windowState) => - BlocBuilder( + previous.selection != current.selection, + builder: (context, toolState) => + BlocBuilder( + buildWhen: (previous, current) => + previous.hideUi != current.hideUi, + builder: (context, inputState) => + BlocBuilder( buildWhen: (previous, current) => - previous.toolbarPosition != current.toolbarPosition || - previous.toolbarSize != current.toolbarSize || - previous.toolbarRows != current.toolbarRows || - previous.navigationRail != current.navigationRail || - previous.navigatorPosition != - current.navigatorPosition || - previous.optionsPanelPosition != - current.optionsPanelPosition || - previous.zoomPosition != current.zoomPosition || - previous.propertyPosition != current.propertyPosition, - builder: (context, settings) { - return LayoutBuilder( - builder: (context, constraints) => _buildLayout( - context, - constraints, - state, - currentIndex, - windowState, - settings, + previous.embedding != current.embedding, + builder: (context, saveState) => + BlocBuilder( + builder: (context, windowState) => + BlocBuilder( + buildWhen: (previous, current) => + previous.toolbarPosition != + current.toolbarPosition || + previous.toolbarSize != + current.toolbarSize || + previous.toolbarRows != + current.toolbarRows || + previous.navigationRail != + current.navigationRail || + previous.navigatorPosition != + current.navigatorPosition || + previous.optionsPanelPosition != + current.optionsPanelPosition || + previous.zoomPosition != + current.zoomPosition || + previous.propertyPosition != + current.propertyPosition, + builder: (context, settings) { + return LayoutBuilder( + builder: (context, constraints) => + _buildLayout( + context, + constraints, + state, + toolState, + inputState, + saveState, + windowState, + settings, + ), + ); + }, + ), ), - ); - }, ), ), ), @@ -698,7 +791,9 @@ class _MainBody extends StatelessWidget { BuildContext context, BoxConstraints constraints, DocumentState state, - CurrentIndex currentIndex, + ToolRuntimeState toolState, + EditorInputState inputState, + DocumentSaveState saveState, WindowState windowState, ButterflySettings settings, ) { @@ -719,18 +814,18 @@ class _MainBody extends StatelessWidget { direction: settings.toolbarPosition.axis, ); final navigatorRailEnabled = - settings.navigationRail || currentIndex.embedding != null; + settings.navigationRail || saveState.embedding != null; final showNavigator = isLarge && navigatorRailEnabled && !windowState.fullScreen && state is DocumentLoadSuccess && - currentIndex.hideUi == HideState.visible; + inputState.hideUi == HideState.visible; return Stack( children: [ const MainViewViewport(), - _buildSelectionListener(context, currentIndex), + _buildSelectionListener(context, toolState), SafeArea( child: Row( textDirection: TextDirection.ltr, @@ -740,18 +835,18 @@ class _MainBody extends StatelessWidget { const NavigatorView(), if (settings.toolbarPosition == ToolbarPosition.left && !isMobile && - currentIndex.hideUi == HideState.visible) + inputState.hideUi == HideState.visible) toolbar, _buildCenterColumn( context, settings, windowState, - currentIndex, + inputState, isMobile, toolbar, ), if (settings.toolbarPosition == ToolbarPosition.right && - currentIndex.hideUi == HideState.visible) + inputState.hideUi == HideState.visible) toolbar, if (showNavigator && settings.navigatorPosition == NavigatorPosition.right) @@ -765,15 +860,15 @@ class _MainBody extends StatelessWidget { Widget _buildSelectionListener( BuildContext context, - CurrentIndex currentIndex, + ToolRuntimeState toolState, ) { return Listener( - behavior: currentIndex.pinned || currentIndex.selection == null + behavior: toolState.pinned || toolState.selection == null ? HitTestBehavior.translucent : HitTestBehavior.opaque, onPointerUp: (details) { - if (currentIndex.pinned) return; - context.read().resetSelection(); + if (toolState.pinned) return; + context.read().resetSelection(); }, ); } @@ -782,7 +877,7 @@ class _MainBody extends StatelessWidget { BuildContext context, ButterflySettings settings, WindowState windowState, - CurrentIndex currentIndex, + EditorInputState inputState, bool isMobile, Widget toolbar, ) { @@ -793,10 +888,10 @@ class _MainBody extends StatelessWidget { pos == ToolbarPosition.inline || pos == ToolbarPosition.top) && !isMobile) && - currentIndex.hideUi == HideState.visible; + inputState.hideUi == HideState.visible; final shareToolbarAndZoomEdge = !isMobile && - currentIndex.hideUi == HideState.visible && + inputState.hideUi == HideState.visible && _toolbarAndZoomShareVerticalEdge(settings); final combineTopToolbarAndZoom = showToolbar && shareToolbarAndZoomEdge; final combineBottomToolbarAndZoom = @@ -811,7 +906,7 @@ class _MainBody extends StatelessWidget { ? _buildCombinedToolbarAndZoom(settings, toolbar) : toolbar, if (optPos == OptionsPanelPosition.top && - currentIndex.hideUi == HideState.visible) + inputState.hideUi == HideState.visible) const ToolbarView(), Expanded( child: Stack( @@ -820,7 +915,7 @@ class _MainBody extends StatelessWidget { context, settings, isMobile, - currentIndex, + inputState, hideZoomTools: combineTopToolbarAndZoom || combineBottomToolbarAndZoom, ), @@ -837,10 +932,10 @@ class _MainBody extends StatelessWidget { ), ), if (optPos == OptionsPanelPosition.bottom && - currentIndex.hideUi == HideState.visible) + inputState.hideUi == HideState.visible) const ToolbarView(), if ((isMobile || pos == ToolbarPosition.bottom) && - currentIndex.hideUi == HideState.visible) + inputState.hideUi == HideState.visible) combineBottomToolbarAndZoom ? _buildCombinedToolbarAndZoom(settings, toolbar) : toolbar, @@ -888,7 +983,7 @@ class _MainBody extends StatelessWidget { BuildContext context, ButterflySettings settings, bool isMobile, - CurrentIndex currentIndex, { + EditorInputState inputState, { bool hideZoomTools = false, }) { return Padding( @@ -912,12 +1007,12 @@ class _MainBody extends StatelessWidget { }, children: [ if (!hideZoomTools) _buildZoomToolsRow(settings, isMobile), - if (currentIndex.hideUi == HideState.touch) + if (inputState.hideUi == HideState.touch) FloatingActionButton.small( tooltip: AppLocalizations.of(context).exit, child: const Icon(PhosphorIconsLight.door), onPressed: () { - context.read().exitHideUI(); + context.read().exitHideUI(); }, ), ], diff --git a/app/lib/views/navigator/areas.dart b/app/lib/views/navigator/areas.dart index e2fa75175624..41580b9a0463 100644 --- a/app/lib/views/navigator/areas.dart +++ b/app/lib/views/navigator/areas.dart @@ -1,5 +1,5 @@ import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/dialogs/area/context.dart'; import 'package:butterfly/dialogs/area/init.dart'; @@ -72,7 +72,8 @@ class _AreasViewState extends State { Offset position; if (config.positionMode == AreaPositionMode.currentCenter) { final center = context - .read() + .read() + .rendererCubit .state .cameraViewport .toRect() @@ -218,14 +219,12 @@ class _AreasViewState extends State { @override Widget build(BuildContext context) { final bloc = context.read(); - return BlocBuilder( + return BlocBuilder( buildWhen: (previous, current) => - previous.cameraViewport != current.cameraViewport || - previous.areaNavigatorCreate != current.areaNavigatorCreate || - previous.areaNavigatorExact != current.areaNavigatorExact || - previous.areaNavigatorAsk != current.areaNavigatorAsk, - builder: (context, currentIndex) { - final viewport = currentIndex.cameraViewport; + previous.cameraViewport != current.cameraViewport, + builder: (context, rendererState) { + final viewState = context.watch().state; + final viewport = rendererState.cameraViewport; final viewportRect = viewport.toRect(); return BlocBuilder( buildWhen: (previous, current) => @@ -264,28 +263,28 @@ class _AreasViewState extends State { ); }).toList(); - final currentIndexCubit = context.read(); + final editorController = context.read(); bool enableButton(int dx, int dy) { if (current == null) return false; - return currentIndex.areaNavigatorCreate || - currentIndexCubit.getRelativeArea(current, dx, dy) != null; + return viewState.areaNavigatorCreate || + editorController.getRelativeArea(current, dx, dy) != null; } bool selectedButton(int dx, int dy) { if (current == null) return false; - return currentIndexCubit.getRelativeArea(current, dx, dy, true) != + return editorController.getRelativeArea(current, dx, dy, true) != null; } Future navigateToRelativeArea(int dx, int dy) async { - await currentIndexCubit.navigateToRelativeArea( + await editorController.navigateToRelativeArea( dx, dy, createAreaName: () => createAreaName( context, state.page, - currentIndex.areaNavigatorAsk, + viewState.areaNavigatorAsk, ), ); } @@ -488,28 +487,32 @@ class _AreasViewState extends State { child: MenuAnchor( menuChildren: [ CheckboxMenuButton( - value: currentIndex.areaNavigatorCreate, + value: viewState.areaNavigatorCreate, onChanged: (value) => context - .read() - .setAreaNavigatorCreate(value ?? false), + .read() + .setAreaNavigator( + create: value ?? false, + ), child: Text( LeapLocalizations.of(context).create, ), ), CheckboxMenuButton( - value: currentIndex.areaNavigatorExact, + value: viewState.areaNavigatorExact, onChanged: (value) => context - .read() - .setAreaNavigatorExact(value ?? false), + .read() + .setAreaNavigator( + exact: value ?? false, + ), child: Text( AppLocalizations.of(context).exact, ), ), CheckboxMenuButton( - value: currentIndex.areaNavigatorAsk, + value: viewState.areaNavigatorAsk, onChanged: (value) => context - .read() - .setAreaNavigatorAsk(value ?? false), + .read() + .setAreaNavigator(ask: value ?? false), child: Text( AppLocalizations.of(context).askForName, ), diff --git a/app/lib/views/navigator/components.dart b/app/lib/views/navigator/components.dart index bbaac7026931..bccdaff3845d 100644 --- a/app/lib/views/navigator/components.dart +++ b/app/lib/views/navigator/components.dart @@ -1,5 +1,5 @@ import 'package:butterfly/api/file_system.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/dialogs/packs/components.dart'; import 'package:butterfly/handlers/handler.dart'; import 'package:butterfly_api/butterfly_api.dart'; @@ -69,11 +69,11 @@ class _ComponentsViewState extends State { selectedPacks.isEmpty || selectedPacks.contains(e.namespace), ) .toList(); - return BlocBuilder( + return BlocBuilder( buildWhen: (previous, current) => previous.temporaryHandler != current.temporaryHandler, - builder: (context, currentIndex) { - final handler = currentIndex.temporaryHandler; + builder: (context, toolState) { + final handler = toolState.temporaryHandler; return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -118,7 +118,7 @@ class _ComponentsViewState extends State { handler.data.component == named, key: ValueKey((e.namespace, e.key)), onTap: () => context - .read() + .read() .changeTemporaryHandler( context, StampTool(component: named), diff --git a/app/lib/views/navigator/files.dart b/app/lib/views/navigator/files.dart index 7834099a61a7..4079a9e37d69 100644 --- a/app/lib/views/navigator/files.dart +++ b/app/lib/views/navigator/files.dart @@ -1,6 +1,6 @@ import 'package:butterfly/api/open.dart'; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/embed/embedding.dart'; import 'package:butterfly/views/files/view.dart'; @@ -29,14 +29,14 @@ class _FilesNavigatorPageState extends State { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( buildWhen: (previous, current) => previous.location != current.location || previous.absolute != current.absolute, - builder: (context, state) { + builder: (context, saveState) { AssetLocation? location; - if (state is DocumentLoaded) { - location = state.location; + if (!saveState.location.isEmpty) { + location = saveState.location; location = AssetLocation( remote: location.remote, path: '/${location.path}', diff --git a/app/lib/views/navigator/view.dart b/app/lib/views/navigator/view.dart index 2ed0b4a75af7..9654b82e092c 100644 --- a/app/lib/views/navigator/view.dart +++ b/app/lib/views/navigator/view.dart @@ -1,5 +1,5 @@ import 'package:butterfly/api/open.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/views/navigator/areas.dart'; import 'package:butterfly/views/navigator/components.dart'; @@ -88,15 +88,15 @@ class _NavigatorViewState extends State buildWhen: (previous, current) => previous.navigatorPosition != current.navigatorPosition, builder: (context, settings) => - BlocBuilder( + BlocBuilder( buildWhen: (previous, current) => previous.navigatorEnabled != current.navigatorEnabled || previous.navigatorPage != current.navigatorPage, - builder: (context, currentIndex) { + builder: (context, viewState) { final selected = NavigatorPage.values.indexOf( - currentIndex.navigatorPage, + viewState.navigatorPage, ); - if (currentIndex.navigatorEnabled) { + if (viewState.navigatorEnabled) { _animationController.forward(); } else { _animationController.reverse(); @@ -142,20 +142,20 @@ class _NavigatorViewState extends State ), ) .toList(), - selectedIndex: currentIndex.navigatorEnabled - ? selected - : null, + selectedIndex: viewState.navigatorEnabled ? selected : null, groupAlignment: 0, onDestinationSelected: (index) { - final cubit = context.read(); + final cubit = context.read(); if (selected == index) { - cubit.setNavigatorEnabled( - !currentIndex.navigatorEnabled, + cubit.setNavigator( + enabled: !viewState.navigatorEnabled, ); return; } - cubit.setNavigatorPage(NavigatorPage.values[index]); - cubit.setNavigatorEnabled(true); + cubit.setNavigator( + page: NavigatorPage.values[index], + enabled: true, + ); }, ), ], @@ -179,11 +179,11 @@ class _DocumentNavigatorState extends State with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( buildWhen: (previous, current) => previous.navigatorPage != current.navigatorPage, - builder: (context, currentIndex) { - final page = currentIndex.navigatorPage; + builder: (context, viewState) { + final page = viewState.navigatorPage; final body = switch (page) { NavigatorPage.waypoints => const WaypointsView(), NavigatorPage.areas => const AreasView(), diff --git a/app/lib/views/pen_only_toggle.dart b/app/lib/views/pen_only_toggle.dart index f5698edc4c46..788067d113b3 100644 --- a/app/lib/views/pen_only_toggle.dart +++ b/app/lib/views/pen_only_toggle.dart @@ -1,5 +1,5 @@ import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -16,71 +16,72 @@ class PenOnlyToggle extends StatelessWidget { return BlocBuilder( buildWhen: (previous, current) => previous.runtimeType != current.runtimeType, - builder: (context, docState) => BlocBuilder( - buildWhen: (previous, current) => - previous.penDetected != current.penDetected || - previous.hideUi != current.hideUi || - previous.sessionPenOnlyInput != current.sessionPenOnlyInput, - builder: (context, currentIndex) => - BlocBuilder( - buildWhen: (previous, current) => - previous.penOnlyInput != current.penOnlyInput || - previous.showPenOnlyToggle != current.showPenOnlyToggle, - builder: (context, settings) { - // Don't show if: - // - No pen has been detected - // - UI is hidden - // - Setting to show toggle is disabled - // - Document is not loaded - if (!currentIndex.penDetected || - currentIndex.hideUi != HideState.visible || - !settings.showPenOnlyToggle || - docState is! DocumentLoadSuccess) { - return const SizedBox.shrink(); - } + builder: (context, docState) => + BlocBuilder( + buildWhen: (previous, current) => + previous.penDetected != current.penDetected || + previous.hideUi != current.hideUi || + previous.sessionPenOnlyInput != current.sessionPenOnlyInput, + builder: (context, inputState) => + BlocBuilder( + buildWhen: (previous, current) => + previous.penOnlyInput != current.penOnlyInput || + previous.showPenOnlyToggle != current.showPenOnlyToggle, + builder: (context, settings) { + // Don't show if: + // - No pen has been detected + // - UI is hidden + // - Setting to show toggle is disabled + // - Document is not loaded + if (!inputState.penDetected || + inputState.hideUi != HideState.visible || + !settings.showPenOnlyToggle || + docState is! DocumentLoadSuccess) { + return const SizedBox.shrink(); + } - // Use effective pen-only state (considers both setting and session) - final penOnlyEnabled = context - .read() - .effectivePenOnlyInput; - final isAutoMode = settings.penOnlyInput == null; + // Use effective pen-only state (considers both setting and session) + final penOnlyEnabled = context + .read() + .effectivePenOnlyInput; + final isAutoMode = settings.penOnlyInput == null; - return Tooltip( - message: AppLocalizations.of(context).penOnlyInput, - child: IconButton.filled( - style: IconButton.styleFrom( - backgroundColor: penOnlyEnabled - ? Theme.of(context).colorScheme.primaryContainer - : Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - foregroundColor: penOnlyEnabled - ? Theme.of(context).colorScheme.onPrimaryContainer - : Theme.of(context).colorScheme.onSurfaceVariant, - ), - icon: PhosphorIcon( - penOnlyEnabled - ? PhosphorIconsFill.pen - : PhosphorIconsLight.pen, - ), - onPressed: () { - if (isAutoMode) { - // In auto mode, toggle the session state - context - .read() - .setSessionPenOnlyInput(!penOnlyEnabled); - } else { - // In explicit mode, toggle the persisted setting - context.read().changePenOnlyInput( - !penOnlyEnabled, - ); - } - }, - ), - ); - }, - ), - ), + return Tooltip( + message: AppLocalizations.of(context).penOnlyInput, + child: IconButton.filled( + style: IconButton.styleFrom( + backgroundColor: penOnlyEnabled + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + foregroundColor: penOnlyEnabled + ? Theme.of(context).colorScheme.onPrimaryContainer + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + icon: PhosphorIcon( + penOnlyEnabled + ? PhosphorIconsFill.pen + : PhosphorIconsLight.pen, + ), + onPressed: () { + if (isAutoMode) { + // In auto mode, toggle the session state + context + .read() + .setSessionPenOnlyInput(!penOnlyEnabled); + } else { + // In explicit mode, toggle the persisted setting + context.read().changePenOnlyInput( + !penOnlyEnabled, + ); + } + }, + ), + ); + }, + ), + ), ); } } diff --git a/app/lib/views/property.dart b/app/lib/views/property.dart index 43f924bd20bf..b788397d7ba7 100644 --- a/app/lib/views/property.dart +++ b/app/lib/views/property.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/api/open.dart'; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/dialogs/packs/asset.dart'; import 'package:butterfly/widgets/editable_list_tile.dart'; @@ -53,7 +53,7 @@ class _PropertyViewState extends State @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( buildWhen: (previous, current) => previous.selection?.selected != current.selection?.selected || previous.pinned != current.pinned, @@ -103,7 +103,7 @@ class _PropertyViewState extends State } void _closeView() { - context.read().resetSelection(force: true); + context.read().resetSelection(force: true); } Animation get _offsetAnimation => Tween( @@ -318,7 +318,7 @@ class _PropertyCardState extends State<_PropertyCard> { tooltip: AppLocalizations.of(context).delete, onPressed: () { selection.onDelete(context); - context.read().resetSelection( + context.read().resetSelection( force: true, ); }, @@ -333,7 +333,7 @@ class _PropertyCardState extends State<_PropertyCard> { ), const SizedBox(height: 42, child: VerticalDivider()), if (!widget.isMobile) - BlocBuilder( + BlocBuilder( buildWhen: (previous, current) => previous.pinned != current.pinned, builder: (context, state) => IconButton( @@ -348,7 +348,7 @@ class _PropertyCardState extends State<_PropertyCard> { PhosphorIconsLight.pushPin, ), onPressed: () => - context.read().togglePin(), + context.read().togglePin(), ), ), const SizedBox(width: 8), diff --git a/app/lib/views/toolbar/polygon.dart b/app/lib/views/toolbar/polygon.dart index ec20e92ea3d7..58383ebe77c6 100644 --- a/app/lib/views/toolbar/polygon.dart +++ b/app/lib/views/toolbar/polygon.dart @@ -1,5 +1,5 @@ import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; import 'package:butterfly/views/toolbar/color.dart'; import 'package:butterfly_api/butterfly_api.dart'; @@ -37,7 +37,7 @@ class PolygonToolbarView extends StatelessWidget ), ), onEyeDropper: (context) { - bloc.currentIndexCubit.changeTemporaryHandler( + bloc.editorController.changeTemporaryHandler( context, EyeDropperTool(), bloc: bloc, diff --git a/app/lib/views/toolbar/view.dart b/app/lib/views/toolbar/view.dart index 85084de9f2c9..314b5c741e4a 100644 --- a/app/lib/views/toolbar/view.dart +++ b/app/lib/views/toolbar/view.dart @@ -1,4 +1,4 @@ -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -25,15 +25,14 @@ class _ToolbarViewState extends State { builder: (context, state) { return Align( child: Card( - child: BlocBuilder( + child: BlocBuilder( buildWhen: (previous, current) => previous.temporaryToolbar != current.temporaryToolbar || previous.toolbar != current.toolbar, - builder: (context, currentIndex) { + builder: (context, toolState) { Widget? child; var height = 0.0; - final toolbar = - currentIndex.temporaryToolbar ?? currentIndex.toolbar; + final toolbar = toolState.temporaryToolbar ?? toolState.toolbar; if (toolbar != null) { height = toolbar.preferredSize.height; child = toolbar; diff --git a/app/lib/views/view.dart b/app/lib/views/view.dart index 0b4d0517978d..1ff60b656dcd 100644 --- a/app/lib/views/view.dart +++ b/app/lib/views/view.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:butterfly/actions/shortcuts.dart'; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/handlers/handler.dart'; @@ -68,9 +68,9 @@ class _MainViewViewportState extends State bool _isMousePenOrTouch(PointerDeviceKind kind) => _isMouseOrPen(kind) || kind == PointerDeviceKind.touch; - bool _isTouchMoveGesture(CurrentIndexCubit currentIndex) => - currentIndex.moveEnabled && - currentIndex.state.pointers.every( + bool _isTouchMoveGesture(EditorController controller) => + controller.inputCubit.moveEnabled && + controller.inputCubit.state.pointers.every( (pointer) => _pointerKinds[pointer] == PointerDeviceKind.touch, ); @@ -186,7 +186,7 @@ class _MainViewViewportState extends State void _invokeLongPressShortcut( PointerEvent event, PointerDeviceKind kind, - CurrentIndexCubit cubit, + EditorController cubit, _HandlerGetter getHandler, _EventContextGetter getEventContext, ) { @@ -221,7 +221,7 @@ class _MainViewViewportState extends State String? shortcutId, PointerEvent event, PointerDeviceKind kind, - CurrentIndexCubit cubit, + EditorController cubit, _HandlerGetter getHandler, _EventContextGetter getEventContext, ) { @@ -239,7 +239,7 @@ class _MainViewViewportState extends State void _scheduleMultiTapShortcut( PointerUpEvent event, - CurrentIndexCubit cubit, + EditorController cubit, _HandlerGetter getHandler, _EventContextGetter getEventContext, ) { @@ -270,7 +270,7 @@ class _MainViewViewportState extends State Future _handlePointerDown( PointerDownEvent event, - CurrentIndexCubit cubit, + EditorController cubit, _HandlerGetter getHandler, _EventContextGetter getEventContext, _TemporaryToolChanger changeTemporaryTool, @@ -278,7 +278,7 @@ class _MainViewViewportState extends State // Detect pen/stylus input if (event.kind == PointerDeviceKind.stylus || event.kind == PointerDeviceKind.invertedStylus) { - cubit.setPenDetected(true); + cubit.inputCubit.detectPen(true); } _isScalingDisabled = event.kind == PointerDeviceKind.trackpad ? false @@ -289,11 +289,11 @@ class _MainViewViewportState extends State } _pointerKinds[event.pointer] = event.kind; - cubit.addPointer(event.pointer); - cubit.setButtons(event.buttons); + cubit.inputCubit.addPointer(event.pointer); + cubit.inputCubit.setButtons(event.buttons); final handler = getHandler(); final ruler = RulerHandler.getInteractiveRuler( - cubit.state, + cubit.toolCubit.state, handler, event.localPosition, getEventContext().viewportSize, @@ -313,7 +313,7 @@ class _MainViewViewportState extends State Future _handlePointerMove( PointerMoveEvent event, - CurrentIndexCubit cubit, + EditorController cubit, DocumentLoaded state, _HandlerGetter getHandler, _EventContextGetter getEventContext, @@ -334,12 +334,12 @@ class _MainViewViewportState extends State ruler.transformWithPointerMove(getEventContext(), event); return; } - final currentIndexState = cubit.state; + final inputState = cubit.inputCubit.state; if (_isTouchMoveGesture(cubit)) { - if (currentIndexState.pointers.isEmpty) { + if (inputState.pointers.isEmpty) { return; } - if (event.pointer == currentIndexState.pointers.first) { + if (event.pointer == inputState.pointers.first) { final transform = context.read().state; cubit.move( -event.delta / transform.size, @@ -358,7 +358,7 @@ class _MainViewViewportState extends State Future _handlePointerUp( PointerUpEvent event, - CurrentIndexCubit cubit, + EditorController cubit, _HandlerGetter getHandler, _EventContextGetter getEventContext, ) async { @@ -368,21 +368,21 @@ class _MainViewViewportState extends State if (!wasRulerInteraction && (_isScalingDisabled ?? true)) { await getHandler().onPointerUp(event, getEventContext()); } - cubit.removePointer(event.pointer); + cubit.inputCubit.removePointer(event.pointer); _pointerKinds.remove(event.pointer); if (wasRulerInteraction) { - cubit.removeButtons(); + cubit.inputCubit.removeButtons(); } else { _scheduleMultiTapShortcut(event, cubit, getHandler, getEventContext); } } - void _handlePointerCancel(PointerCancelEvent event, CurrentIndexCubit cubit) { + void _handlePointerCancel(PointerCancelEvent event, EditorController cubit) { _resetRulerInteraction(); - cubit.removePointer(event.pointer); + cubit.inputCubit.removePointer(event.pointer); _pointerKinds.remove(event.pointer); - cubit.removeButtons(); - if (cubit.state.pointers.isEmpty) { + cubit.inputCubit.removeButtons(); + if (cubit.inputCubit.state.pointers.isEmpty) { _isScalingDisabled = null; } } @@ -415,7 +415,8 @@ class _MainViewViewportState extends State final blocState = bloc.state; if (blocState is! DocumentLoadSuccess) return; if (state != AppLifecycleState.resumed) { - context.read().resetInput(bloc); + final controller = context.read(); + controller.toolCubit.resetInput(bloc, controller.inputCubit); } } @@ -463,7 +464,7 @@ class _MainViewViewportState extends State InputMapping? nextPointerMapping; final settings = context.read().state; final config = settings.inputConfiguration; - final cubit = context.read(); + final cubit = context.read(); // Mapped to the priority of the buttons switch (kind) { case PointerDeviceKind.touch: @@ -506,7 +507,7 @@ class _MainViewViewportState extends State } if (nextPointerMapping.getCategory() == InputMappingCategory.handTool) { - cubit.changeTemporaryHandlerMove(); + cubit.toolCubit.changeTemporaryHandlerMove(cubit.rendererCubit); } else { final int? index = nextPointerMapping.getToolPositionIndex(); if (index != null) { @@ -526,311 +527,336 @@ class _MainViewViewportState extends State } var point = Offset.zero; - final CurrentIndexCubit cubit = context - .read(); + final EditorController cubit = context.read(); Handler getHandler() { if (state is DocumentPresentationState) return state.handler; return cubit.getHandler(); } - return BlocBuilder( + return BlocBuilder( buildWhen: (previous, current) => previous.cameraViewport != current.cameraViewport || - previous.foregrounds != current.foregrounds || - previous.handler != current.handler || - previous.temporaryHandler != current.temporaryHandler || - previous.toggleableForegrounds != - current.toggleableForegrounds || - previous.temporaryForegrounds != - current.temporaryForegrounds || previous.rendererStates != current.rendererStates || - previous.networkingForegrounds != - current.networkingForegrounds || previous.temporaryRendererStates != - current.temporaryRendererStates || - previous.cursor != current.cursor || - previous.temporaryCursor != current.temporaryCursor, - builder: (context, currentIndex) { - var realSize = currentIndex.cameraViewport.toRealSize(); - final viewportSize = constraints.biggest; - final isSimiliar = - (realSize.width - viewportSize.width).abs() < 2 && - (realSize.height - viewportSize.height).abs() < 2; - if (state is DocumentLoadSuccess && !isSimiliar) { - WidgetsBinding.instance.addPostFrameCallback( - (_) => bake(), - ); - } - return Actions( - actions: getHandler().getActions(context), - child: DefaultTextEditingShortcuts( - child: Focus( - child: MouseRegion( - cursor: currentIndex.currentCursor, - child: Builder( - builder: (context) { - EventContext getEventContext() { - return EventContext( - context, - constraints.biggest, - _isShiftPressed, - _isAltPressed, - _isCtrlPressed, - ); - } - - return GestureDetector( - onTapUp: (details) async { - getHandler().onTapUp( - details, - getEventContext(), - ); - cubit.removeButtons(); - cubit.resetReleaseHandler(bloc); - }, - onTapDown: (details) => getHandler() - .onTapDown(details, getEventContext()), - onSecondaryTapUp: (details) => - getHandler().onSecondaryTapUp( - details, - getEventContext(), - ), - onScaleUpdate: (details) { - final handler = getHandler(); - if (_ruler != null) { - _ruler?.transformWithScaleUpdate( - getEventContext(), - details, - ); - return; - } - if (_isScalingDisabled ?? true) { - handler.onScaleUpdate( - details, - getEventContext(), - ); - return; - } - final cubit = context - .read(); - final settings = context - .read() - .state; - if (cubit.fetchHandler() == - null && - !settings.inputGestures) { - return; - } - var current = details.scale; - current = current - size; - var sensitivity = context - .read() - .state - .gestureSensitivity; - if (details.scale == 1) { - cubit.move( - -details.focalPointDelta / - sensitivity / - cubit.transformCubit.state.size, - currentArea: state.currentArea, - ); - } else { - cubit.zoom( - current / sensitivity + 1, - point, + current.temporaryRendererStates, + builder: (context, rendererState) { + return BlocBuilder( + buildWhen: (previous, current) => + previous.foregrounds != current.foregrounds || + previous.handler != current.handler || + previous.temporaryHandler != + current.temporaryHandler || + previous.toggleableForegrounds != + current.toggleableForegrounds || + previous.temporaryForegrounds != + current.temporaryForegrounds || + previous.networkingForegrounds != + current.networkingForegrounds || + previous.cursor != current.cursor || + previous.temporaryCursor != current.temporaryCursor, + builder: (context, toolState) { + var realSize = rendererState.cameraViewport + .toRealSize(); + final viewportSize = constraints.biggest; + final isSimiliar = + (realSize.width - viewportSize.width).abs() < 2 && + (realSize.height - viewportSize.height).abs() < 2; + if (state is DocumentLoadSuccess && !isSimiliar) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => bake(), + ); + } + return Actions( + actions: getHandler().getActions(context), + child: DefaultTextEditingShortcuts( + child: Focus( + child: MouseRegion( + cursor: toolState.currentCursor, + child: Builder( + builder: (context) { + EventContext getEventContext() { + return EventContext( + context, + constraints.biggest, + _isShiftPressed, + _isAltPressed, + _isCtrlPressed, ); } - size = details.scale; - if (!settings.hasFlag('smoothNavigation')) { - delayBake(); - } - }, - onLongPressEnd: (details) => - getHandler().onLongPressEnd( - details, - getEventContext(), - ), - onScaleEnd: (details) { - if (_ruler != null) { - _resetRulerInteraction(); - _isScalingDisabled = null; - cubit.removeButtons(); - return; - } - getHandler().onScaleEnd( - details, - getEventContext(), - ); - if (!(_isScalingDisabled ?? true)) { - final settings = context - .read() - .state; - final sensitivity = - settings.gestureSensitivity; - cubit.slide( - details.velocity.pixelsPerSecond / - sensitivity / - cubit.transformCubit.state.size, - details.scaleVelocity, - currentArea: state.currentArea, - ); - if (!settings.hasFlag( - 'smoothNavigation', - )) { - delayBake(); - } - } - _resetRulerInteraction(); - cubit.removeButtons(); - if (_isScalingDisabled ?? true) { - cubit.resetReleaseHandler(bloc); - } - }, - onScaleStart: (details) { - _isScalingDisabled ??= !_isTouchMoveGesture( - cubit, - ); - _ruler = RulerHandler.getInteractiveRuler( - currentIndex, - cubit.getHandler(), - details.localFocalPoint, - constraints.biggest, - ); - if (_ruler != null) { - _isScalingDisabled = false; - _ruler?.beginTransform( - details.localFocalPoint, - ); - } else if (_isScalingDisabled != false) { - _isScalingDisabled = cubit - .getHandler() - .onScaleStart( + + return GestureDetector( + onTapUp: (details) async { + getHandler().onTapUp( + details, + getEventContext(), + ); + cubit.inputCubit.removeButtons(); + cubit.resetReleaseHandler(bloc); + }, + onTapDown: (details) => + getHandler().onTapDown( details, getEventContext(), + ), + onSecondaryTapUp: (details) => + getHandler().onSecondaryTapUp( + details, + getEventContext(), + ), + onScaleUpdate: (details) { + final handler = getHandler(); + if (_ruler != null) { + _ruler?.transformWithScaleUpdate( + getEventContext(), + details, ); - } else { - cubit.getHandler().onScaleStartAbort( - details, - getEventContext(), - ); - } - point = details.localFocalPoint; - size = 1; - }, - onLongPressStart: (details) => - getHandler().onLongPressStart( - details, - getEventContext(), - ), - onLongPressDown: (details) => - getHandler().onLongPressDown( - details, - getEventContext(), - ), - child: Listener( - onPointerSignal: (pointerSignal) { - if (state is! DocumentLoadSuccess) return; - if (pointerSignal is PointerScrollEvent) { - // dx and dy are the delta between the last scroll event - var dx = pointerSignal.scrollDelta.dx; - var dy = pointerSignal.scrollDelta.dy; - // Get zoom by dx and dy - var scale = pointerSignal.size; + return; + } + if (_isScalingDisabled ?? true) { + handler.onScaleUpdate( + details, + getEventContext(), + ); + return; + } + final cubit = context + .read(); final settings = context .read() .state; - var sensitivity = - settings.scrollSensitivity; - scale /= -sensitivity * 100; - scale += 1; - dx /= sensitivity; - dy /= sensitivity; - final cubit = context - .read(); - final transform = context - .read() - .state; - if (_mouseState == _MouseState.scale) { - // Calculate the new scale using dx and dy - scale = -(dx + dy / 2) / 100 + 1; - cubit.zoom( - scale, - pointerSignal.localPosition, + if (cubit + .fetchHandler< + SelectHandler + >() == + null && + !settings.inputGestures) { + return; + } + var current = details.scale; + current = current - size; + var sensitivity = context + .read() + .state + .gestureSensitivity; + if (details.scale == 1) { + cubit.move( + -details.focalPointDelta / + sensitivity / + cubit.transformCubit.state.size, + currentArea: state.currentArea, ); } else { - cubit - ..move( - (_mouseState == - _MouseState.inverse - ? Offset(dy, dx) - : Offset(dx, dy)) / - transform.size, - currentArea: state.currentArea, - ) - ..zoom( - scale, - pointerSignal.localPosition, - ); + cubit.zoom( + current / sensitivity + 1, + point, + ); } + size = details.scale; if (!settings.hasFlag( 'smoothNavigation', )) { delayBake(); } - } - }, - onPointerPanZoomStart: (event) { - _isScalingDisabled = false; - }, - onPointerDown: (event) => - _handlePointerDown( - event, - cubit, - getHandler, - getEventContext, - changeTemporaryTool, - ), - onPointerUp: (event) => _handlePointerUp( - event, - cubit, - getHandler, - getEventContext, - ), - behavior: HitTestBehavior.translucent, - onPointerHover: (event) { - cubit.updateLastPosition( - event.localPosition, - ); - getHandler().onPointerHover( - event, - getEventContext(), - ); - }, - onPointerMove: (event) => - _handlePointerMove( - event, + }, + onLongPressEnd: (details) => + getHandler().onLongPressEnd( + details, + getEventContext(), + ), + onScaleEnd: (details) { + if (_ruler != null) { + _resetRulerInteraction(); + _isScalingDisabled = null; + cubit.inputCubit.removeButtons(); + return; + } + getHandler().onScaleEnd( + details, + getEventContext(), + ); + if (!(_isScalingDisabled ?? true)) { + final settings = context + .read() + .state; + final sensitivity = + settings.gestureSensitivity; + cubit.slide( + details.velocity.pixelsPerSecond / + sensitivity / + cubit.transformCubit.state.size, + details.scaleVelocity, + currentArea: state.currentArea, + ); + if (!settings.hasFlag( + 'smoothNavigation', + )) { + delayBake(); + } + } + _resetRulerInteraction(); + cubit.inputCubit.removeButtons(); + if (_isScalingDisabled ?? true) { + cubit.resetReleaseHandler(bloc); + } + }, + onScaleStart: (details) { + _isScalingDisabled ??= + !_isTouchMoveGesture(cubit); + _ruler = + RulerHandler.getInteractiveRuler( + toolState, + cubit.getHandler(), + details.localFocalPoint, + constraints.biggest, + ); + if (_ruler != null) { + _isScalingDisabled = false; + _ruler?.beginTransform( + details.localFocalPoint, + ); + } else if (_isScalingDisabled != + false) { + _isScalingDisabled = cubit + .getHandler() + .onScaleStart( + details, + getEventContext(), + ); + } else { + cubit.getHandler().onScaleStartAbort( + details, + getEventContext(), + ); + } + point = details.localFocalPoint; + size = 1; + }, + onLongPressStart: (details) => + getHandler().onLongPressStart( + details, + getEventContext(), + ), + onLongPressDown: (details) => + getHandler().onLongPressDown( + details, + getEventContext(), + ), + child: Listener( + onPointerSignal: (pointerSignal) { + if (state is! DocumentLoadSuccess) { + return; + } + if (pointerSignal + is PointerScrollEvent) { + // dx and dy are the delta between the last scroll event + var dx = + pointerSignal.scrollDelta.dx; + var dy = + pointerSignal.scrollDelta.dy; + // Get zoom by dx and dy + var scale = pointerSignal.size; + final settings = context + .read() + .state; + var sensitivity = + settings.scrollSensitivity; + scale /= -sensitivity * 100; + scale += 1; + dx /= sensitivity; + dy /= sensitivity; + final cubit = context + .read(); + final transform = context + .read() + .state; + if (_mouseState == + _MouseState.scale) { + // Calculate the new scale using dx and dy + scale = -(dx + dy / 2) / 100 + 1; + cubit.zoom( + scale, + pointerSignal.localPosition, + ); + } else { + cubit + ..move( + (_mouseState == + _MouseState + .inverse + ? Offset(dy, dx) + : Offset(dx, dy)) / + transform.size, + currentArea: + state.currentArea, + ) + ..zoom( + scale, + pointerSignal.localPosition, + ); + } + if (!settings.hasFlag( + 'smoothNavigation', + )) { + delayBake(); + } + } + }, + onPointerPanZoomStart: (event) { + _isScalingDisabled = false; + }, + onPointerDown: (event) => + _handlePointerDown( + event, + cubit, + getHandler, + getEventContext, + changeTemporaryTool, + ), + onPointerUp: (event) => + _handlePointerUp( + event, + cubit, + getHandler, + getEventContext, + ), + behavior: HitTestBehavior.translucent, + onPointerHover: (event) { + cubit.updateLastPosition( + event.localPosition, + ); + getHandler().onPointerHover( + event, + getEventContext(), + ); + }, + onPointerMove: (event) => + _handlePointerMove( + event, + cubit, + state, + getHandler, + getEventContext, + delayBake, + ), + onPointerCancel: (event) => + _handlePointerCancel(event, cubit), + child: _buildCanvas( + rendererState, + toolState, cubit, state, - getHandler, - getEventContext, delayBake, ), - onPointerCancel: (event) => - _handlePointerCancel(event, cubit), - child: _buildCanvas( - currentIndex, - cubit, - state, - delayBake, - ), - ), - ); - }, + ), + ); + }, + ), + ), ), ), - ), - ), + ); + }, ); }, ); @@ -843,8 +869,9 @@ class _MainViewViewportState extends State } Widget _buildCanvas( - CurrentIndex currentIndex, - CurrentIndexCubit cubit, + RendererRuntimeState rendererState, + ToolRuntimeState toolState, + EditorController cubit, DocumentLoaded state, VoidCallback delayBake, ) { @@ -901,20 +928,20 @@ class _MainViewViewportState extends State CustomPaint( size: Size.infinite, foregroundPainter: ForegroundPainter( - currentIndex.getAllForegrounds(), + toolState.getAllForegrounds(), state.data, state.page, state.info, ColorScheme.of(context), frictionTransform, - cubit.state.selection, + toolState.selection, state.settingsCubit.state.navigatorPosition, ), painter: ViewPainter( state.data, state.page, state.info, - cameraViewport: currentIndex.cameraViewport, + cameraViewport: rendererState.cameraViewport, transform: frictionTransform, invisibleLayers: state.invisibleLayers, currentArea: state.currentArea, diff --git a/app/lib/views/zoom.dart b/app/lib/views/zoom.dart index 539dc30ea6e5..53fba72b117f 100644 --- a/app/lib/views/zoom.dart +++ b/app/lib/views/zoom.dart @@ -1,5 +1,5 @@ import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/helpers/number.dart'; @@ -66,16 +66,17 @@ class _ZoomViewState extends State with TickerProviderStateMixin { void _zoom(double value, [bool bake = true]) { final documentState = context.read().state; - final currentIndexCubit = context.read(); - final currentIndex = currentIndexCubit.state; + final editorController = context.read(); + final rendererState = editorController.rendererCubit.state; + final inputState = editorController.inputCubit.state; if (documentState is! DocumentLoaded) { return; } - final size = currentIndex.cameraViewport.toRealSize(); + final size = rendererState.cameraViewport.toRealSize(); final center = Offset(size.width / 2, size.height / 2); - currentIndexCubit.size(value, center, true); + editorController.size(value, center, true); if (bake) { - currentIndexCubit.bake(documentState); + editorController.bake(documentState); } final settings = context.read().state; @@ -83,7 +84,7 @@ class _ZoomViewState extends State with TickerProviderStateMixin { final hideZoom = !settings.zoomEnabled || windowState.fullScreen || - currentIndex.hideUi != HideState.visible; + inputState.hideUi != HideState.visible; if ((!_focusNode.hasFocus && widget.isMobile) || hideZoom) { _controller.reverse(); } @@ -105,12 +106,12 @@ class _ZoomViewState extends State with TickerProviderStateMixin { previous.size != current.size, builder: (context, transform) { var scale = transform.size; - final currentIndexCubit = context - .read(); + final editorController = context.read(); final hideZoom = !settings.zoomEnabled || windowState.fullScreen || - currentIndexCubit.state.hideUi != HideState.visible; + editorController.inputCubit.state.hideUi != + HideState.visible; final body = StatefulBuilder( builder: (context, setState) { diff --git a/app/lib/widgets/search.dart b/app/lib/widgets/search.dart index 70937fb5eeff..6571b4031e84 100644 --- a/app/lib/widgets/search.dart +++ b/app/lib/widgets/search.dart @@ -1,4 +1,5 @@ import 'package:butterfly/bloc/document_bloc.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/helpers/point.dart'; import 'package:butterfly/visualizer/element.dart'; import 'package:butterfly/visualizer/tool.dart'; @@ -106,7 +107,7 @@ class SearchButton extends StatelessWidget { onTap: () { final state = bloc.state; if (state is! DocumentLoaded) return; - final cubit = bloc.currentIndexCubit; + final cubit = bloc.editorController; final position = result.getPosition(); final page = result.getPage(); if (page != null) { @@ -117,7 +118,7 @@ class SearchButton extends StatelessWidget { } cubit.bake(state); if (result is ToolResult) { - cubit.resetSelection(); + cubit.toolCubit.resetSelection(); cubit.changeTool(bloc, index: result.index, context: context); } Navigator.pop(context); diff --git a/app/pubspec.lock b/app/pubspec.lock index d5f03fd63e73..37754bdca21d 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -289,7 +289,7 @@ packages: source: hosted version: "0.3.5+4" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 7873f2b8dadc..030da982a661 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -33,6 +33,7 @@ dependencies: intl: any path_provider: ^2.1.3 shared_preferences: ^2.2.3 + crypto: ^3.0.7 url_launcher: ^6.2.6 phosphor_flutter: ^2.1.0 replay_bloc: ^0.3.0 diff --git a/app/test/bloc/document_bloc_test.dart b/app/test/bloc/document_bloc_test.dart index 96649aec6fbc..abe4781ffc09 100644 --- a/app/test/bloc/document_bloc_test.dart +++ b/app/test/bloc/document_bloc_test.dart @@ -4,7 +4,7 @@ import 'dart:typed_data'; import 'package:archive/archive.dart'; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/models/viewport.dart'; @@ -49,7 +49,7 @@ class _VisibleTrackingRenderer extends Renderer { @override Future onVisible( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, DocumentLoaded blocState, CameraTransform renderTransform, Size size, @@ -61,7 +61,7 @@ class _VisibleTrackingRenderer extends Renderer { @override Future onHidden( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, DocumentLoaded blocState, CameraTransform renderTransform, Size size, @@ -88,8 +88,8 @@ class _VisibleTrackingRenderer extends Renderer { } } -class _BlockingReloadCurrentIndexCubit extends CurrentIndexCubit { - _BlockingReloadCurrentIndexCubit( +class _BlockingReloadEditorController extends EditorController { + _BlockingReloadEditorController( super.settingsCubit, super.transformCubit, super.viewport, @@ -117,7 +117,7 @@ class _ThrowingVisibleRenderer extends Renderer { @override Future onVisible( - CurrentIndexCubit currentIndexCubit, + EditorController editorController, DocumentLoaded blocState, CameraTransform renderTransform, Size size, @@ -167,7 +167,7 @@ void main() { late MockButterflyFileSystem fileSystem; late MockSettingsCubit settingsCubit; - late CurrentIndexCubit currentIndexCubit; + late EditorController editorController; late WindowCubit windowCubit; late DocumentBloc bloc; @@ -180,7 +180,7 @@ void main() { ).thenReturn(const ButterflySettings(autosave: false)); when(() => settingsCubit.stream).thenAnswer((_) => const Stream.empty()); - currentIndexCubit = CurrentIndexCubit( + editorController = EditorController( settingsCubit, TransformCubit(1), CameraViewport.unbaked(), @@ -202,7 +202,7 @@ void main() { bloc = DocumentBloc( fileSystem, - currentIndexCubit, + editorController, windowCubit, data, const AssetLocation(path: 'test-note.bfly'), @@ -216,8 +216,8 @@ void main() { if (!bloc.isClosed) { await bloc.close(); } - if (!currentIndexCubit.isClosed) { - await currentIndexCubit.close(); + if (!editorController.isClosed) { + await editorController.close(); } if (!windowCubit.isClosed) { await windowCubit.close(); @@ -327,7 +327,7 @@ void main() { bloc.add(ToolsReplaced([activeTool, otherTool])); await _settleBlocEvents(); - currentIndexCubit.changeIndex(0); + editorController.toolCubit.setIndex(0); bloc.add( ToolsChanged([PenTool(property: const PenProperty(strokeWidth: 12))]), @@ -345,7 +345,7 @@ void main() { test('reset state change waits for reload to finish', () async { await bloc.close(); - await currentIndexCubit.close(); + await editorController.close(); final firstElement = ShapeElement( id: 'first-page-element', @@ -374,7 +374,7 @@ void main() { final (secondData, secondPageName) = data.setPage(secondPage, 'Page 2'); data = secondData; final secondRenderer = Renderer.fromInstance(secondElement); - final blockingCubit = _BlockingReloadCurrentIndexCubit( + final blockingCubit = _BlockingReloadEditorController( settingsCubit, TransformCubit(1), CameraViewport.unbaked( @@ -385,10 +385,10 @@ void main() { height: 100, ), ); - currentIndexCubit = blockingCubit; + editorController = blockingCubit; bloc = DocumentBloc( fileSystem, - currentIndexCubit, + editorController, windowCubit, data, const AssetLocation(path: 'test-note.bfly'), @@ -418,7 +418,7 @@ void main() { test('current index close disposes initially loaded renderers', () async { await bloc.close(); - await currentIndexCubit.close(); + await editorController.close(); final element = ShapeElement( id: 'initial-renderer', @@ -434,7 +434,7 @@ void main() { var data = NoteData(Archive()); final (newData, pageName) = data.setPage(page, 'Page'); data = newData; - currentIndexCubit = CurrentIndexCubit( + editorController = EditorController( settingsCubit, TransformCubit(1), CameraViewport.unbaked( @@ -445,7 +445,7 @@ void main() { ); bloc = DocumentBloc( fileSystem, - currentIndexCubit, + editorController, windowCubit, data, const AssetLocation(path: 'test-note.bfly'), @@ -455,15 +455,15 @@ void main() { ); await bloc.load(); - expect(currentIndexCubit.renderers, contains(same(renderer))); + expect(editorController.rendererCubit.renderers, contains(same(renderer))); - await currentIndexCubit.close(); + await editorController.close(); expect(renderer.disposeCalls, 1); }); test('failed visible renderer is retried', () async { await bloc.close(); - await currentIndexCubit.close(); + await editorController.close(); final element = ShapeElement( id: 'failed-visible', @@ -479,7 +479,7 @@ void main() { var data = NoteData(Archive()); final (nextData, pageName) = data.setPage(page, 'Page 1'); data = nextData; - currentIndexCubit = CurrentIndexCubit( + editorController = EditorController( settingsCubit, TransformCubit(1), CameraViewport.unbaked( @@ -492,7 +492,7 @@ void main() { ); bloc = DocumentBloc( fileSystem, - currentIndexCubit, + editorController, windowCubit, data, const AssetLocation(path: 'test-note.bfly'), @@ -501,14 +501,14 @@ void main() { pageName, ); - await currentIndexCubit.loadElements(bloc.state); - await currentIndexCubit.loadElements(bloc.state); + await editorController.loadElements(bloc.state); + await editorController.loadElements(bloc.state); expect(renderer.onVisibleCalls, 2); }); test('pdf renderer catches document load failures', () async { - await currentIndexCubit.close(); + await editorController.close(); final element = PdfElement(source: 'test.pdf', width: 100, height: 100); final renderer = PdfRenderer(element, 'layer'); @@ -521,7 +521,7 @@ void main() { final (nextData, pageName) = data.setPage(page, 'Page 1'); data = nextData; final assetService = _ThrowingPdfAssetService(); - currentIndexCubit = CurrentIndexCubit( + editorController = EditorController( settingsCubit, TransformCubit(1), CameraViewport.unbaked( @@ -539,11 +539,11 @@ void main() { fileSystem: fileSystem, windowCubit: windowCubit, assetService: assetService, - absolute: currentIndexCubit.state.absolute, + absolute: editorController.saveCubit.state.absolute, ); await renderer.onVisible( - currentIndexCubit, + editorController, docState, const CameraTransform(), const Size(100, 100), @@ -621,24 +621,29 @@ void main() { expect(state.invisibleLayers, contains(layers[1].id)); expect(state.invisibleLayers, isNot(contains(layers[3].id))); expect( - currentIndexCubit.renderers.map((renderer) => renderer.layer), + editorController.rendererCubit.renderers.map( + (renderer) => renderer.layer, + ), containsAll([layers[1].id, layers[3].id]), ); }); test('resetInput clears active pointers and buttons', () async { - currentIndexCubit.addPointer(12); - currentIndexCubit.setButtons(kPrimaryMouseButton); + editorController.inputCubit.addPointer(12); + editorController.inputCubit.setButtons(kPrimaryMouseButton); - await currentIndexCubit.resetInput(bloc); + await editorController.toolCubit.resetInput( + bloc, + editorController.inputCubit, + ); - expect(currentIndexCubit.state.pointers, isEmpty); - expect(currentIndexCubit.state.buttons, isNull); + expect(editorController.inputCubit.state.pointers, isEmpty); + expect(editorController.inputCubit.state.buttons, isNull); }); test('adding a combined highlight immediately unbakes its group', () async { await bloc.close(); - await currentIndexCubit.close(); + await editorController.close(); final existingElement = PenElement( id: 'existing', @@ -654,7 +659,7 @@ void main() { var data = NoteData(Archive()); final (nextData, pageName) = data.setPage(page, 'Page 1'); data = nextData; - currentIndexCubit = CurrentIndexCubit( + editorController = EditorController( settingsCubit, TransformCubit(1), CameraViewport.unbaked( @@ -667,7 +672,7 @@ void main() { ); bloc = DocumentBloc( fileSystem, - currentIndexCubit, + editorController, windowCubit, data, const AssetLocation(path: 'test-note.bfly'), @@ -675,13 +680,16 @@ void main() { page, pageName, ); - await currentIndexCubit.bake( + await editorController.bake( bloc.state as DocumentLoadSuccess, viewportSize: const Size(100, 100), pixelRatio: 1, reset: true, ); - expect(currentIndexCubit.state.cameraViewport.bakedElements, isNotEmpty); + expect( + editorController.rendererCubit.state.cameraViewport.bakedElements, + isNotEmpty, + ); bloc.add( ElementsCreated([ @@ -694,7 +702,7 @@ void main() { ); await _settleBlocEvents(); - final viewport = currentIndexCubit.state.cameraViewport; + final viewport = editorController.rendererCubit.state.cameraViewport; expect(viewport.bakedElements, isEmpty); expect( viewport.unbakedElements.whereType().map( @@ -707,7 +715,7 @@ void main() { test('bake records only elements visible in the current viewport', () async { await bloc.close(); - await currentIndexCubit.close(); + await editorController.close(); final visibleElement = ShapeElement( id: 'visible', @@ -731,7 +739,7 @@ void main() { var data = NoteData(Archive()); final (nextData, pageName) = data.setPage(page, 'Page 1'); data = nextData; - currentIndexCubit = CurrentIndexCubit( + editorController = EditorController( settingsCubit, TransformCubit(1), CameraViewport.unbaked( @@ -744,7 +752,7 @@ void main() { ); bloc = DocumentBloc( fileSystem, - currentIndexCubit, + editorController, windowCubit, data, const AssetLocation(path: 'test-note.bfly'), @@ -753,14 +761,14 @@ void main() { pageName, ); - await currentIndexCubit.bake( + await editorController.bake( bloc.state as DocumentLoadSuccess, viewportSize: const Size(100, 100), pixelRatio: 1, reset: true, ); - final viewport = currentIndexCubit.state.cameraViewport; + final viewport = editorController.rendererCubit.state.cameraViewport; expect(viewport.baked, isTrue); expect( viewport.visibleElements.map((renderer) => renderer.element.id), @@ -775,7 +783,7 @@ void main() { test('bake refreshes cached viewport when pixel ratio changes', () async { await bloc.close(); - await currentIndexCubit.close(); + await editorController.close(); final element = ShapeElement( id: 'visible', @@ -791,7 +799,7 @@ void main() { var data = NoteData(Archive()); final (nextData, pageName) = data.setPage(page, 'pixel-ratio-page'); data = nextData; - currentIndexCubit = CurrentIndexCubit( + editorController = EditorController( settingsCubit, TransformCubit(1), CameraViewport.unbaked( @@ -804,7 +812,7 @@ void main() { ); bloc = DocumentBloc( fileSystem, - currentIndexCubit, + editorController, windowCubit, data, const AssetLocation(path: 'test-note.bfly'), @@ -813,29 +821,32 @@ void main() { pageName, ); - await currentIndexCubit.bake( + await editorController.bake( bloc.state as DocumentLoadSuccess, viewportSize: const Size(100, 100), pixelRatio: 1, reset: true, ); - expect(currentIndexCubit.state.cameraViewport.pixelRatio, 1); - expect(currentIndexCubit.state.cameraViewport.unbakedElements, isEmpty); + expect(editorController.rendererCubit.state.cameraViewport.pixelRatio, 1); + expect( + editorController.rendererCubit.state.cameraViewport.unbakedElements, + isEmpty, + ); - await currentIndexCubit.bake( + await editorController.bake( bloc.state as DocumentLoadSuccess, viewportSize: const Size(100, 100), pixelRatio: 2, ); - expect(currentIndexCubit.state.cameraViewport.pixelRatio, 2); + expect(editorController.rendererCubit.state.cameraViewport.pixelRatio, 2); }); test( 'bake snaps cached viewport to screen pixels at fractional zoom', () async { await bloc.close(); - await currentIndexCubit.close(); + await editorController.close(); when(() => settingsCubit.state).thenReturn( const ButterflySettings( autosave: false, @@ -859,7 +870,7 @@ void main() { data = nextData; final transformCubit = TransformCubit(1); transformCubit.teleport(const Offset(12.3, 45.6), 1.37); - currentIndexCubit = CurrentIndexCubit( + editorController = EditorController( settingsCubit, transformCubit, CameraViewport.unbaked( @@ -872,7 +883,7 @@ void main() { ); bloc = DocumentBloc( fileSystem, - currentIndexCubit, + editorController, windowCubit, data, const AssetLocation(path: 'test-note.bfly'), @@ -881,15 +892,15 @@ void main() { pageName, ); - await currentIndexCubit.bake( + await editorController.bake( bloc.state as DocumentLoadSuccess, viewportSize: const Size(401, 303), pixelRatio: 1, reset: true, ); - final viewport = currentIndexCubit.state.cameraViewport; - final transform = currentIndexCubit.transformCubit.state; + final viewport = editorController.rendererCubit.state.cameraViewport; + final transform = editorController.transformCubit.state; final left = (viewport.x - transform.position.dx) * transform.size; final top = (viewport.y - transform.position.dy) * transform.size; final right = left + viewport.width!; @@ -906,7 +917,7 @@ void main() { test('renderImage does not hide already tracked visible renderers', () async { await bloc.close(); - await currentIndexCubit.close(); + await editorController.close(); final element = ShapeElement( id: 'visible', @@ -923,7 +934,7 @@ void main() { var data = NoteData(Archive()); final (nextData, pageName) = data.setPage(page, 'Page 1'); data = nextData; - currentIndexCubit = CurrentIndexCubit( + editorController = EditorController( settingsCubit, TransformCubit(1), CameraViewport.unbaked( @@ -936,7 +947,7 @@ void main() { ); bloc = DocumentBloc( fileSystem, - currentIndexCubit, + editorController, windowCubit, data, const AssetLocation(path: 'test-note.bfly'), @@ -945,7 +956,7 @@ void main() { pageName, ); - await currentIndexCubit.bake( + await editorController.bake( bloc.state as DocumentLoadSuccess, viewportSize: const Size(100, 100), pixelRatio: 1, @@ -953,14 +964,14 @@ void main() { ); renderer.onVisibleCalls = 0; renderer.onHiddenCalls = 0; - await currentIndexCubit.renderImage( + await editorController.renderImage( (bloc.state as DocumentLoadSuccess).data, page, (bloc.state as DocumentLoadSuccess).info, const ImageExportOptions(width: 100, height: 100), docState: bloc.state as DocumentLoadSuccess, ); - await currentIndexCubit.bake( + await editorController.bake( bloc.state as DocumentLoadSuccess, viewportSize: const Size(100, 100), pixelRatio: 1, @@ -975,7 +986,7 @@ void main() { 'renderImage passes quality as pixel ratio without offsetting scale', () async { await bloc.close(); - await currentIndexCubit.close(); + await editorController.close(); final element = ShapeElement( id: 'aligned', @@ -998,7 +1009,7 @@ void main() { var data = NoteData(Archive()); final (nextData, pageName) = data.setPage(page, 'Page 1'); data = nextData; - currentIndexCubit = CurrentIndexCubit( + editorController = EditorController( settingsCubit, TransformCubit(1), CameraViewport.unbaked( @@ -1011,7 +1022,7 @@ void main() { ); bloc = DocumentBloc( fileSystem, - currentIndexCubit, + editorController, windowCubit, data, const AssetLocation(path: 'test-note.bfly'), @@ -1020,7 +1031,7 @@ void main() { pageName, ); - final image = await currentIndexCubit.renderImage( + final image = await editorController.renderImage( data, page, (bloc.state as DocumentLoadSuccess).info, diff --git a/app/test/cubits/editor_session_test.dart b/app/test/cubits/editor_session_test.dart new file mode 100644 index 000000000000..7f3029e00be0 --- /dev/null +++ b/app/test/cubits/editor_session_test.dart @@ -0,0 +1,165 @@ +import 'package:butterfly/api/file_system.dart'; +import 'package:butterfly/cubits/editor_session.dart'; +import 'package:butterfly/cubits/transform.dart'; +import 'package:butterfly/models/persisted_document_state.dart'; +import 'package:butterfly/views/navigator/view.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lw_file_system/lw_file_system.dart'; + +import '../helpers/mocks.dart'; + +void main() { + group('PersistedDocumentState', () { + test('round trips through typed encoding', () { + final state = PersistedDocumentState( + pathKey: 'path/a', + contentHash: 'hash', + pageName: 'Page 1', + camera: const PersistedCameraState( + positionX: 10, + positionY: 20, + zoom: 2, + ), + utilities: const UtilitiesState(lockZoom: true), + selectedTool: const PersistedToolSelection( + toolId: 'tool-a', + toolIndex: 3, + ), + navigatorEnabled: true, + navigatorPage: NavigatorPage.layers.name, + currentLayer: 'layer-a', + currentCollection: 'collection-a', + invisibleLayers: {'hidden-a'}, + areaNavigatorCreate: false, + updatedAt: DateTime.utc(2026), + ); + + final decoded = decodePersistedDocumentState( + encodePersistedDocumentState(state), + ); + + expect(decoded, state); + }); + + test('uses schema defaults for sparse json', () { + final state = PersistedDocumentState.fromJson(const {}); + + expect(state.version, kPersistedDocumentStateVersion); + expect(state.camera.zoom, 1); + expect(state.utilities, const UtilitiesState()); + expect(state.navigatorPage, NavigatorPage.waypoints.name); + }); + }); + + group('EditorSessionCubit storage', () { + late DocumentStateFileSystem fileSystem; + + setUp(() { + fileSystem = buildMockDocumentStateFileSystem(); + }); + + test('content hash match wins over path match', () async { + const byContent = PersistedDocumentState(pageName: 'Content Page'); + const byPath = PersistedDocumentState(pageName: 'Path Page'); + await fileSystem.initialize(); + await fileSystem.createFile(documentStateContentKey('hash-a'), byContent); + await fileSystem.createFile('path/a', byPath); + + final loaded = await EditorSessionCubit.load( + fileSystem: fileSystem, + contentHash: 'hash-a', + pathKey: 'path/a', + ); + + expect(loaded?.pageName, 'Content Page'); + }); + + test('falls back to path when content hash is missing', () async { + const byPath = PersistedDocumentState(pageName: 'Path Page'); + await fileSystem.initialize(); + await fileSystem.createFile('path/a', byPath); + + final loaded = await EditorSessionCubit.load( + fileSystem: fileSystem, + contentHash: 'missing', + pathKey: 'path/a', + ); + + expect(loaded?.pageName, 'Path Page'); + }); + + test('returns null when no fingerprints match', () async { + final loaded = await EditorSessionCubit.load( + fileSystem: fileSystem, + contentHash: 'missing', + pathKey: 'path/missing', + ); + + expect(loaded, isNull); + }); + + test('writes session state to content and path keys', () async { + final transformCubit = TransformCubit(1); + final cubit = EditorSessionCubit( + fileSystem: fileSystem, + transformCubit: transformCubit, + initialState: const PersistedDocumentState(pageName: 'Page 1'), + pathKey: 'path/a', + contentHash: 'hash-a', + ); + + cubit.updateNavigator(enabled: true, page: NavigatorPage.layers); + await cubit.saveNow(); + + expect( + (await fileSystem.getFile( + documentStateContentKey('hash-a'), + ))?.navigatorPage, + NavigatorPage.layers.name, + ); + final contentRecord = await fileSystem.getFile( + documentStateContentKey('hash-a'), + ); + final pathRecord = await fileSystem.getFile('path/a'); + expect(contentRecord?.pathKey, 'path/a'); + expect(contentRecord?.contentHash, 'hash-a'); + expect(pathRecord?.pathKey, 'path/a'); + expect(pathRecord?.contentHash, 'hash-a'); + expect(pathRecord?.navigatorEnabled, isTrue); + + await cubit.close(); + await transformCubit.close(); + }); + + test('does not modify document files when session state changes', () async { + final documentSystem = buildMockDocumentFileSystem(); + final original = NoteFile(Uint8List.fromList([1, 2, 3])); + await documentSystem.initialize(); + await documentSystem.createFile('/test.bfly', original); + + final transformCubit = TransformCubit(1); + final cubit = EditorSessionCubit( + fileSystem: fileSystem, + transformCubit: transformCubit, + initialState: const PersistedDocumentState(pageName: 'Page 1'), + pathKey: 'path/test', + contentHash: 'hash-test', + ); + + cubit.updateAreaNavigator(create: false); + await cubit.saveNow(); + + final asset = await documentSystem.getAsset('/test.bfly'); + expect(asset, isA>()); + expect( + (asset as FileSystemFile).data?.data, + orderedEquals(original.data), + ); + + await cubit.close(); + await transformCubit.close(); + }); + }); +} diff --git a/app/test/handlers/polygon_handler_test.dart b/app/test/handlers/polygon_handler_test.dart index d534ec15de90..c6f11f681644 100644 --- a/app/test/handlers/polygon_handler_test.dart +++ b/app/test/handlers/polygon_handler_test.dart @@ -1,6 +1,6 @@ import 'package:archive/archive.dart'; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/handlers/handler.dart'; @@ -25,7 +25,7 @@ void main() { late MockButterflyFileSystem fileSystem; late MockSettingsCubit settingsCubit; - late CurrentIndexCubit currentIndexCubit; + late EditorController editorController; late WindowCubit windowCubit; DocumentBloc? bloc; @@ -38,7 +38,7 @@ void main() { ).thenReturn(const ButterflySettings(autosave: false)); when(() => settingsCubit.stream).thenAnswer((_) => const Stream.empty()); - currentIndexCubit = CurrentIndexCubit( + editorController = EditorController( settingsCubit, TransformCubit(1), const CameraViewport.unbaked(), @@ -51,8 +51,8 @@ void main() { if (currentBloc != null && !currentBloc.isClosed) { await currentBloc.close(); } - if (!currentIndexCubit.isClosed) { - await currentIndexCubit.close(); + if (!editorController.isClosed) { + await editorController.close(); } windowCubit.close(); }); @@ -79,7 +79,7 @@ void main() { final (data, pageName) = NoteData(Archive()).setPage(page, 'Page 1'); bloc = DocumentBloc( fileSystem, - currentIndexCubit, + editorController, windowCubit, data, const AssetLocation(path: 'test-note.bfly'), diff --git a/app/test/handlers/shape_handler_test.dart b/app/test/handlers/shape_handler_test.dart index 95691cfc0cfe..5d43e3cf1c65 100644 --- a/app/test/handlers/shape_handler_test.dart +++ b/app/test/handlers/shape_handler_test.dart @@ -1,17 +1,17 @@ import 'dart:ui'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/handlers/handler.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -class MockCurrentIndexCubit extends Mock implements CurrentIndexCubit {} +class MockEditorController extends Mock implements EditorController {} void main() { test('does not create shapes with zero size', () { final handler = ShapeHandler(ShapeTool()); - final cubit = MockCurrentIndexCubit(); + final cubit = MockEditorController(); expect( handler.transformElements( @@ -27,7 +27,7 @@ void main() { final handler = ShapeHandler( ShapeTool(property: const ShapeProperty(shape: LineShape())), ); - final cubit = MockCurrentIndexCubit(); + final cubit = MockEditorController(); expect( handler.transformElements(const Rect.fromLTRB(10, 10, 10, 20), '', cubit), diff --git a/app/test/helpers/mocks.dart b/app/test/helpers/mocks.dart index 75dbd2abbf95..d6e0b4c22773 100644 --- a/app/test/helpers/mocks.dart +++ b/app/test/helpers/mocks.dart @@ -3,6 +3,7 @@ import 'package:archive/archive.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/models/defaults.dart'; +import 'package:butterfly/models/persisted_document_state.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:butterfly_api/src/models/text.dart'; import 'package:mocktail/mocktail.dart'; @@ -14,6 +15,8 @@ class MockButterflyFileSystem implements ButterflyFileSystem { final DocumentFileSystem _documentFileSystem = buildMockDocumentFileSystem(); final TemplateFileSystem _templateFileSystem = buildMockTemplateFileSystem(); final PackFileSystem _packFileSystem = buildMockPackFileSystem(); + final DocumentStateFileSystem _documentStateFileSystem = + buildMockDocumentStateFileSystem(); final SettingsCubit _settingsCubit; MockButterflyFileSystem({SettingsCubit? settingsCubit}) @@ -43,6 +46,12 @@ class MockButterflyFileSystem implements ButterflyFileSystem { bool forceRecreate = false, ]) => _documentFileSystem; + @override + DocumentStateFileSystem buildDocumentStateSystem([ + ExternalStorage? storage, + bool forceRecreate = false, + ]) => _documentStateFileSystem; + @override TypedKeyFileSystem buildPackSystem([ ExternalStorage? storage, @@ -115,6 +124,9 @@ class MockButterflyFileSystem implements ButterflyFileSystem { @override void removeCachedDocumentSystem(ExternalStorage? storage) {} + @override + void removeCachedDocumentStateSystem(ExternalStorage? storage) {} + @override void removeCachedFileSystem(ExternalStorage? storage) {} @@ -163,3 +175,10 @@ PackFileSystem buildMockPackFileSystem() { onDecode: decodeNoteData, ); } + +DocumentStateFileSystem buildMockDocumentStateFileSystem() { + return MockTypedKeyFileSystem( + onEncode: encodePersistedDocumentState, + onDecode: decodePersistedDocumentState, + ); +} diff --git a/app/test/renderers/image_renderer_test.dart b/app/test/renderers/image_renderer_test.dart index 79d45d662483..4c2492198339 100644 --- a/app/test/renderers/image_renderer_test.dart +++ b/app/test/renderers/image_renderer_test.dart @@ -1,14 +1,14 @@ import 'dart:ui' as ui; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/renderers/renderer.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -class MockCurrentIndexCubit extends Mock implements CurrentIndexCubit {} +class MockEditorController extends Mock implements EditorController {} class MockDocumentLoaded extends Mock implements DocumentLoaded {} @@ -38,7 +38,7 @@ void main() { ); renderer.onHidden( - MockCurrentIndexCubit(), + MockEditorController(), MockDocumentLoaded(), const CameraTransform(), ui.Size.zero, diff --git a/app/test/views/navigator/layers_test.dart b/app/test/views/navigator/layers_test.dart index 5765932c0c6e..c1376c5df7e3 100644 --- a/app/test/views/navigator/layers_test.dart +++ b/app/test/views/navigator/layers_test.dart @@ -1,6 +1,6 @@ import 'package:archive/archive.dart'; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/models/viewport.dart'; @@ -29,7 +29,7 @@ void main() { ).thenReturn(const ButterflySettings(autosave: false)); when(() => settingsCubit.stream).thenAnswer((_) => const Stream.empty()); - final currentIndexCubit = CurrentIndexCubit( + final editorController = EditorController( settingsCubit, TransformCubit(1), CameraViewport.unbaked(), @@ -45,7 +45,7 @@ void main() { final (data, pageName) = NoteData(Archive()).setPage(page, 'Page'); final bloc = DocumentBloc( fileSystem, - currentIndexCubit, + editorController, windowCubit, data, const AssetLocation(path: 'layers-test.bfly'), @@ -55,7 +55,7 @@ void main() { ); addTearDown(() async { await bloc.close(); - await currentIndexCubit.close(); + await editorController.close(); await windowCubit.close(); }); diff --git a/app/test/views/navigator/pages_test.dart b/app/test/views/navigator/pages_test.dart index 490ef989f216..efeeedd30e0a 100644 --- a/app/test/views/navigator/pages_test.dart +++ b/app/test/views/navigator/pages_test.dart @@ -1,6 +1,6 @@ import 'package:archive/archive.dart'; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/dialogs/pages.dart' as pages_dialog; @@ -110,7 +110,7 @@ void main() { () => settingsCubit.state, ).thenReturn(const ButterflySettings(autosave: false)); when(() => settingsCubit.stream).thenAnswer((_) => const Stream.empty()); - final currentIndexCubit = CurrentIndexCubit( + final editorController = EditorController( settingsCubit, TransformCubit(1), CameraViewport.unbaked(), @@ -143,7 +143,7 @@ void main() { final page = data.getPage(thirdPath)!; final bloc = DocumentBloc( fileSystem, - currentIndexCubit, + editorController, windowCubit, data, const AssetLocation(path: 'pages-test.bfly'), @@ -153,7 +153,7 @@ void main() { ); addTearDown(() async { await bloc.close(); - await currentIndexCubit.close(); + await editorController.close(); await windowCubit.close(); }); diff --git a/app/test/views/project_page_lifecycle_test.dart b/app/test/views/project_page_lifecycle_test.dart index 1183571b98b8..e23649961626 100644 --- a/app/test/views/project_page_lifecycle_test.dart +++ b/app/test/views/project_page_lifecycle_test.dart @@ -1,7 +1,7 @@ import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/api/open.dart'; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/models/defaults.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; @@ -89,8 +89,6 @@ void main() { 'Scaffold: ${find.byType(Scaffold).evaluate().length}, ' 'DocumentBloc create/close: ' '${observer.documentBlocCreates}/${observer.documentBlocCloses}, ' - 'CurrentIndexCubit create/close: ' - '${observer.currentIndexCubitCreates}/${observer.currentIndexCubitCloses}, ' 'events: ${observer.events}, ' 'exception: $exception)', ); @@ -166,9 +164,7 @@ void main() { ); } - testWidgets('replacing document route closes document bloc and index cubit', ( - tester, - ) async { + testWidgets('replacing document route closes document bloc', (tester) async { await tester.pumpWidget(buildApp()); for (var i = 1; i <= 3; i++) { @@ -177,8 +173,7 @@ void main() { tester, () => find.byType(ProjectPage).evaluate().isNotEmpty && - observer.documentBlocCreates == i && - observer.currentIndexCubitCreates == i, + observer.documentBlocCreates == i, 'document open $i', ); await tester.pump(const Duration(seconds: 1)); @@ -189,31 +184,28 @@ void main() { () => find.byType(ProjectPage).evaluate().isEmpty && find.byType(ProjectPage, skipOffstage: false).evaluate().isEmpty && - observer.documentBlocCloses == i && - observer.currentIndexCubitCloses == i, + observer.documentBlocCloses == i, 'document close $i', ); } expect(observer.documentBlocCreates, 3); expect(observer.documentBlocCloses, 3); - expect(observer.currentIndexCubitCreates, 3); - expect(observer.currentIndexCubitCloses, 3); }); testWidgets('converted imported file starts unsaved', (tester) async { await tester.pumpWidget(buildApp()); router.go('/import'); - await pumpUntil( - tester, - () => - find.byType(ProjectPage).evaluate().isNotEmpty && - observer.lastCurrentIndexCubit != null, - 'imported document open', - ); + await pumpUntil( + tester, + () => + find.byType(ProjectPage).evaluate().isNotEmpty && + observer.lastSaveCubit != null, + 'imported document open', + ); - expect(observer.lastCurrentIndexCubit!.state.saved, SaveState.unsaved); + expect(observer.lastSaveCubit!.state.saved, SaveState.unsaved); router.go('/'); await pumpUntil( @@ -222,7 +214,7 @@ void main() { find.byType(ProjectPage).evaluate().isEmpty && find.byType(ProjectPage, skipOffstage: false).evaluate().isEmpty && observer.documentBlocCloses == 1 && - observer.currentIndexCubitCloses == 1, + observer.saveCubitCloses == 1, 'imported document close', ); }); @@ -231,9 +223,9 @@ void main() { class _LifecycleObserver extends BlocObserver { int documentBlocCreates = 0; int documentBlocCloses = 0; - int currentIndexCubitCreates = 0; - int currentIndexCubitCloses = 0; - CurrentIndexCubit? lastCurrentIndexCubit; + int saveCubitCreates = 0; + int saveCubitCloses = 0; + DocumentSaveCubit? lastSaveCubit; final events = []; @override @@ -241,9 +233,9 @@ class _LifecycleObserver extends BlocObserver { super.onCreate(bloc); if (bloc is DocumentBloc) { documentBlocCreates++; - } else if (bloc is CurrentIndexCubit) { - currentIndexCubitCreates++; - lastCurrentIndexCubit = bloc; + } else if (bloc is DocumentSaveCubit) { + saveCubitCreates++; + lastSaveCubit = bloc; } } @@ -251,8 +243,8 @@ class _LifecycleObserver extends BlocObserver { void onClose(BlocBase bloc) { if (bloc is DocumentBloc) { documentBlocCloses++; - } else if (bloc is CurrentIndexCubit) { - currentIndexCubitCloses++; + } else if (bloc is DocumentSaveCubit) { + saveCubitCloses++; } super.onClose(bloc); } From d47e35cb965a80d86e3dda011d91b86b35763798 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 30 Jun 2026 09:46:21 +0200 Subject: [PATCH 034/117] Move functions in its respected cubit --- app/lib/actions/change_tool.dart | 4 +- app/lib/actions/select.dart | 10 +- app/lib/actions/zoom.dart | 8 +- app/lib/bloc/document_bloc.dart | 75 +- app/lib/cubits/editor_controller.dart | 335 ++- app/lib/cubits/editor_controller_methods.dart | 2425 ----------------- app/lib/cubits/editor_runtime.dart | 1810 +++++++++++- app/lib/cubits/transform.dart | 468 ++++ app/lib/dialogs/collections.dart | 9 +- app/lib/dialogs/elements.dart | 36 +- app/lib/dialogs/export/general.dart | 8 +- app/lib/dialogs/export/pdf.dart | 7 +- app/lib/dialogs/export/thumbnail.dart | 4 +- app/lib/dialogs/import/add.dart | 3 +- app/lib/dialogs/packs/asset.dart | 4 +- app/lib/embed/handler.dart | 5 +- app/lib/handlers/eye_dropper.dart | 13 +- app/lib/handlers/mixins.dart | 7 +- app/lib/handlers/pen.dart | 2 +- app/lib/handlers/polygon.dart | 18 +- app/lib/handlers/presentation.dart | 2 +- app/lib/handlers/select.dart | 4 +- app/lib/handlers/shape.dart | 4 +- app/lib/renderers/elements/polygon.dart | 13 +- app/lib/selections/document.dart | 12 +- app/lib/services/import.dart | 3 +- app/lib/services/network.dart | 1 - app/lib/views/app_bar.dart | 6 +- app/lib/views/edit.dart | 187 +- app/lib/views/navigator/areas.dart | 29 +- app/lib/views/navigator/components.dart | 2 + app/lib/views/toolbar/polygon.dart | 3 +- app/lib/views/view.dart | 177 +- app/lib/views/zoom.dart | 12 +- app/lib/widgets/search.dart | 10 +- app/test/bloc/document_bloc_test.dart | 37 +- 36 files changed, 3067 insertions(+), 2686 deletions(-) delete mode 100644 app/lib/cubits/editor_controller_methods.dart diff --git a/app/lib/actions/change_tool.dart b/app/lib/actions/change_tool.dart index 4e2a4dc7a46b..76a5da12353c 100644 --- a/app/lib/actions/change_tool.dart +++ b/app/lib/actions/change_tool.dart @@ -40,7 +40,9 @@ class ChangeToolAction extends Action { @override Future invoke(ChangeToolIntent intent) async { final bloc = context.read(); - context.read().changeTool( + final editorController = context.read(); + editorController.toolCubit.changeTool( + editorController, bloc, context: context, index: intent.index, diff --git a/app/lib/actions/select.dart b/app/lib/actions/select.dart index 68a72864ac5f..3435bdb4bbc5 100644 --- a/app/lib/actions/select.dart +++ b/app/lib/actions/select.dart @@ -27,10 +27,16 @@ class SelectAllAction extends Action { @override Future invoke(SelectAllIntent intent) async { final cubit = context.read(); - if (cubit.getHandler() is SelectHandler) return; + if (cubit.toolCubit.getHandler( + editable: cubit.saveCubit.state.embedding?.editable != false, + ) + is SelectHandler) { + return; + } final bloc = context.read(); - final handler = await cubit.changeTemporaryHandler( + final handler = await cubit.toolCubit.changeTemporaryHandler( context, + cubit, SelectTool(), bloc: bloc, temporaryState: TemporaryState.removeAfterClick, diff --git a/app/lib/actions/zoom.dart b/app/lib/actions/zoom.dart index 13ed0c33bc11..4c9994b06dd7 100644 --- a/app/lib/actions/zoom.dart +++ b/app/lib/actions/zoom.dart @@ -36,9 +36,13 @@ class ZoomAction extends Action { (viewport.height ?? 0) / 2, ); final transformCubit = cubit.transformCubit; - cubit.size( + cubit.transformCubit.sizeConstrained( transformCubit.state.size + (intent.reverse ? -0.1 : 0.1), - center, + cursor: center, + settingsCubit: cubit.settingsCubit, + rendererCubit: cubit.rendererCubit, + inputCubit: cubit.inputCubit, + viewCubit: cubit.viewCubit, ); } } diff --git a/app/lib/bloc/document_bloc.dart b/app/lib/bloc/document_bloc.dart index cf627c9e3650..773ee9a14954 100644 --- a/app/lib/bloc/document_bloc.dart +++ b/app/lib/bloc/document_bloc.dart @@ -429,10 +429,12 @@ class DocumentBloc extends ReplayBloc { ), ), addedElements: renderers, - shouldRefresh: () => editorController.getHandler().onRenderersCreated( - current.page, - renderers, - ), + shouldRefresh: () => editorController.toolCubit + .getHandler( + editable: + editorController.saveCubit.state.embedding?.editable != false, + ) + .onRenderersCreated(current.page, renderers), ); }); on((event, emit) async { @@ -519,11 +521,12 @@ class DocumentBloc extends ReplayBloc { replacedElements: orderRenderersByPage(newPage, renderers), shouldRefresh: () => replacedRenderers.entries .map( - (element) => cubit.getHandler().onRendererUpdated( - page, - element.key, - element.value, - ), + (element) => cubit.toolCubit + .getHandler( + editable: + cubit.saveCubit.state.embedding?.editable != false, + ) + .onRendererUpdated(page, element.key, element.value), ) .toList() .any((e) => e), @@ -758,11 +761,23 @@ class DocumentBloc extends ReplayBloc { ), ); if (updatedCurrent != null) { - editorController.updateTool(this, updatedCurrent!); + editorController.toolCubit.updateTool( + editorController, + this, + updatedCurrent!, + ); } - editorController.updateTogglingTools(this, changedTools); + editorController.toolCubit.updateTogglingTools( + editorController, + this, + changedTools, + ); if (updatedTemporary != null) { - editorController.updateTemporaryTool(this, updatedTemporary!); + editorController.toolCubit.updateTemporaryTool( + editorController, + this, + updatedTemporary!, + ); } if (selection != null) { editorController.toolCubit.changeSelection(selection); @@ -1013,7 +1028,7 @@ class DocumentBloc extends ReplayBloc { editorController.editorSessionCubit?.updateLayer( invisibleLayers: invisibleLayers, ); - editorController.unbake(newState); + editorController.rendererCubit.unbake(editorController, newState); }); on((event, emit) async { @@ -1551,14 +1566,18 @@ class DocumentBloc extends ReplayBloc { windowCubit: current.windowCubit, absolute: current.absolute, ); - editorController.updateHandler(this, newState.handler); + editorController.toolCubit.updateHandler( + this, + editorController.rendererCubit, + newState.handler, + ); emit(newState); }); on((event, emit) { final current = state; if (current is! DocumentPresentationState) return; emit(current.oldState); - editorController.changeTool(this); + editorController.toolCubit.changeTool(editorController, this); setFullScreen(current.fullScreen); }); on((event, emit) { @@ -1583,7 +1602,7 @@ class DocumentBloc extends ReplayBloc { ), ) .toList(); - editorController.invalidateRenderers(updatedRenderers); + editorController.rendererCubit.invalidateRenderers(updatedRenderers); _saveState( emit, state: current.copyWith(data: data), @@ -1715,7 +1734,7 @@ class DocumentBloc extends ReplayBloc { final current = state; final cubit = _editorController; if (current is! DocumentLoadSuccess || cubit == null) return; - return cubit.refresh(current, allowBake: allowBake); + return cubit.toolCubit.refresh(cubit, current, allowBake: allowBake); } /// Lightweight refresh that only updates foregrounds without rebaking. @@ -1724,16 +1743,16 @@ class DocumentBloc extends ReplayBloc { final current = state; final cubit = _editorController; if (current is! DocumentLoadSuccess || cubit == null) return; - return cubit.refreshForegrounds(current); + return cubit.toolCubit.refreshForegrounds(cubit, current); } /// Ultra-lightweight update for cursor changes only. void updateCursor(MouseCursor cursor) { - _editorController?.updateCursor(cursor); + _editorController?.toolCubit.setCursor(cursor); } Future refreshToolbar() => - _editorController?.refreshToolbar(this) ?? Future.value(); + _editorController?.toolCubit.refreshToolbar(this) ?? Future.value(); Future bake({ Size? viewportSize, @@ -1743,7 +1762,8 @@ class DocumentBloc extends ReplayBloc { final current = state; final cubit = _editorController; if (current is! DocumentLoaded || cubit == null) return; - return cubit.bake( + return cubit.rendererCubit.bake( + cubit, current, viewportSize: viewportSize, pixelRatio: pixelRatio, @@ -1760,7 +1780,8 @@ class DocumentBloc extends ReplayBloc { final current = state; final cubit = _editorController; if (current is! DocumentLoaded || cubit == null) return Future.value(); - return cubit.delayedBake( + return cubit.rendererCubit.delayedBake( + cubit, current, viewportSize: viewportSize, pixelRatio: pixelRatio, @@ -1770,7 +1791,7 @@ class DocumentBloc extends ReplayBloc { } void cancelDelayedBake() { - _editorController?.cancelDelayedBake(); + _editorController?.rendererCubit.cancelDelayedBake(); } Future load() async { @@ -1786,7 +1807,7 @@ class DocumentBloc extends ReplayBloc { } else { cubit.saveCubit.setSaveState(isCreating: true); } - await cubit.loadElements(current); + await cubit.rendererCubit.loadElements(cubit, current); cubit.init(this); } @@ -1806,7 +1827,8 @@ class DocumentBloc extends ReplayBloc { final cubit = _editorController; if (current is! DocumentLoadSuccess || cubit == null) return; final data = await current.saveData(); - final render = await cubit.render( + final render = await cubit.rendererCubit.render( + cubit, current.data, current.page, current.info, @@ -1882,8 +1904,9 @@ class DocumentBloc extends ReplayBloc { AssetLocation? location, bool force = false, bool isAutosave = false, - }) async => await _editorController?.save( + }) async => await _editorController?.saveCubit.save( this, + _editorController!.networkingService, location: location, force: force, isAutosave: isAutosave, diff --git a/app/lib/cubits/editor_controller.dart b/app/lib/cubits/editor_controller.dart index de8a9172a96b..d57800c1e380 100644 --- a/app/lib/cubits/editor_controller.dart +++ b/app/lib/cubits/editor_controller.dart @@ -1,38 +1,25 @@ import 'dart:async'; -import 'dart:math'; -import 'dart:ui' as ui; -import 'package:butterfly/api/image.dart'; import 'package:butterfly/bloc/document_bloc.dart'; import 'package:butterfly/cubits/editor_session.dart'; import 'package:butterfly/cubits/editor_runtime.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/transform.dart'; -import 'package:butterfly/helpers/rect.dart'; -import 'package:butterfly/helpers/xml.dart'; import 'package:butterfly/renderers/cursors/user.dart'; import 'package:butterfly/renderers/renderer.dart'; import 'package:butterfly/services/network.dart'; import 'package:butterfly/services/logger.dart'; -import 'package:butterfly/views/navigator/constants.dart'; import 'package:butterfly/views/navigator/view.dart'; import 'package:butterfly/visualizer/tool.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:image/image.dart' as img; -import 'package:lw_file_system/lw_file_system.dart'; import 'package:material_leap/material_leap.dart'; import 'package:networker/networker.dart'; -import 'package:pdfrx/pdfrx.dart'; -import 'package:xml/xml.dart'; import '../embed/embedding.dart'; import '../handlers/handler.dart'; import '../models/viewport.dart'; -import '../view_painter.dart'; export 'editor_runtime.dart' show @@ -51,8 +38,6 @@ export 'editor_runtime.dart' ToolCubit, ToolRuntimeState; -part 'editor_controller_methods.dart'; - class EditorController { final SettingsCubit settingsCubit; final TransformCubit transformCubit; @@ -136,10 +121,324 @@ class EditorController { bool get isClosed => _closed; + DocumentBloc? get activeDocumentBloc { + final bloc = _documentBloc?.target; + if (bloc == null || bloc.isClosed) return null; + return bloc; + } + + DocumentLoaded? get activeDocumentState { + final state = activeDocumentBloc?.state; + return state is DocumentLoaded ? state : null; + } + Future reload(DocumentBloc bloc, [DocumentLoaded? blocState]) => reloadRuntime(bloc, blocState); -} -Future _toFile((NoteData, bool) args) async { - return args.$1.toFile(isTextBased: args.$2); + void _onTransformChanged(CameraTransform transform) { + // Debounce transform changes to avoid excessive updates during pan/zoom + _transformDebounceTimer?.cancel(); + _transformDebounceTimer = Timer(const Duration(milliseconds: 16), () { + rendererCubit.updateVisibleElements(this, activeDocumentBloc); + }); + } + + void init(DocumentBloc bloc) { + _documentBloc = WeakReference(bloc); + final blocState = bloc.state; + final index = blocState is DocumentLoadSuccess + ? editorSessionCubit?.resolveToolIndex(blocState.info) + : toolCubit.state.index; + toolCubit.changeTool(this, bloc, index: index ?? 0); + networkingService.setup(bloc); + } + + void _onToolChanged(ToolRuntimeState next) { + if (_isClosing) { + return; + } + final current = _previousToolState; + _previousToolState = next; + if (current == null) return; + + if (next.foregrounds != current.foregrounds || + next.temporaryForegrounds != current.temporaryForegrounds) { + _networkingDebounceTimer?.cancel(); + _networkingDebounceTimer = Timer(const Duration(milliseconds: 50), () { + if (!isClosed) _sendNetworkingState(); + }); + } + } + + void _onInputChanged(EditorInputState next) { + if (_isClosing) return; + final current = _previousInputState; + _previousInputState = next; + if (next.lastPosition != current.lastPosition) { + _networkingDebounceTimer?.cancel(); + _networkingDebounceTimer = Timer(const Duration(milliseconds: 50), () { + if (!isClosed) _sendNetworkingState(); + }); + } + } + + void _onViewChanged(EditorViewState next) { + if (_isClosing) return; + final current = _previousViewState; + _previousViewState = next; + if (current != null && next.userName != current.userName) { + _networkingDebounceTimer?.cancel(); + _networkingDebounceTimer = Timer(const Duration(milliseconds: 50), () { + if (!isClosed) _sendNetworkingState(); + }); + } + } + + void _onRendererChanged(RendererRuntimeState next) { + if (_isClosing) return; + final current = _previousRendererState; + _previousRendererState = next; + final currentViewport = current.cameraViewport; + final newViewport = next.cameraViewport; + + if (!identical(currentViewport, newViewport) && + currentViewport != newViewport) { + toolCubit.state.handler.onViewportUpdated(currentViewport, newViewport); + toolCubit.state.temporaryHandler?.onViewportUpdated( + currentViewport, + newViewport, + ); + } + + currentViewport.disposeImages(except: newViewport); + } + + void _sendNetworkingState({ + List>? foregrounds, + Offset? cursor, + }) { + cursor ??= inputCubit.state.lastPosition ?? Offset.zero; + networkingService.sendUser( + NetworkingUser( + cursor: transformCubit.state.localToGlobal(cursor).toPoint(), + foreground: (foregrounds ?? toolCubit.state.getAllForegrounds(false)) + .map((e) => e.element) + .whereType() + .toList(), + name: networkingService.userName, + ), + ); + } + + Future updateNetworkingState( + DocumentBloc bloc, [ + Map? current, + ]) async { + talker.verbose('Updating networking state'); + final blocState = bloc.state; + if (blocState is! DocumentLoadSuccess) return; + final users = (current ?? networkingService.users).entries.toList(); + final usersByChannel = {for (final entry in users) entry.key: entry.value}; + final activeForegroundElements = users + .expand((entry) => entry.value.foreground ?? const []) + .toSet(); + + final foregrounds = toolCubit.state.networkingForegrounds.toList(); + foregrounds.removeWhere((renderer) { + bool shouldRemove; + if (renderer is UserCursor) { + final activeUser = usersByChannel[renderer.userId]; + shouldRemove = activeUser == null || activeUser != renderer.element; + } else { + shouldRemove = !activeForegroundElements.contains(renderer.element); + } + if (shouldRemove) { + renderer.dispose(); + } + return shouldRemove; + }); + + final existingElements = foregrounds.map((e) => e.element).toSet(); + final added = []; + added.addAll( + users + .expand((entry) => entry.value.foreground ?? const []) + .where((element) => !existingElements.contains(element)) + .map((element) => Renderer.fromInstance(element)), + ); + added.addAll( + users + .where( + (entry) => + foregrounds.whereType().firstWhereOrNull( + (cursor) => + cursor.userId == entry.key && + cursor.element == entry.value, + ) == + null, + ) + .map((entry) => UserCursor(entry.value, entry.key)), + ); + await Future.wait( + added.map( + (e) async => await e.setup( + transformCubit, + blocState.data, + blocState.assetService, + blocState.page, + ), + ), + ); + foregrounds.addAll(added); + toolCubit.setForegrounds(networkingForegrounds: foregrounds); + } + + Future close() async { + if (_closed) return; + _isClosing = true; + _closed = true; + final bloc = activeDocumentBloc; + if (bloc != null) { + await toolCubit.disposeRuntime(bloc); + } + _documentBloc = null; + await rendererCubit.disposeRuntime(); + await _transformSubscription?.cancel(); + _transformSubscription = null; + await _rendererSubscription?.cancel(); + _rendererSubscription = null; + await _toolSubscription?.cancel(); + _toolSubscription = null; + await _inputSubscription?.cancel(); + _inputSubscription = null; + await _viewSubscription?.cancel(); + _viewSubscription = null; + _transformDebounceTimer?.cancel(); + _transformDebounceTimer = null; + _networkingDebounceTimer?.cancel(); + _networkingDebounceTimer = null; + await rendererCubit.close(); + await toolCubit.close(); + await inputCubit.close(); + await saveCubit.close(); + await viewCubit.close(); + if (!networkingService.isClosed) { + await networkingService.close(); + } + } + + /// If addedElements is null, the viewport gets unbaked + Future stateChanged( + DocumentLoadSuccess current, + DocumentBloc bloc, { + DocumentLoadSuccess? oldState, + List> addedElements = const [], + List>? replacedElements, + List>? backgrounds, + bool reset = false, + bool unbake = false, + bool Function()? shouldRefresh, + bool updateIndex = false, + }) async { + rendererCubit.cancelDelayedBake(); + for (var renderer in { + ...?backgrounds, + ...?replacedElements, + ...addedElements, + }) { + await renderer.setup( + transformCubit, + current.data, + current.assetService, + current.page, + ); + } + final blocState = bloc.state; + if (blocState is! DocumentLoadSuccess) return; + toolCubit.state.handler.onDocumentUpdated(blocState, oldState); + + final addsCombinedHighlight = addedElements.any( + (renderer) => + renderer is PenRenderer && renderer.element.combineId != null, + ); + if (replacedElements != null) { + await rendererCubit.replaceUnbaked(this, blocState, [ + ...replacedElements, + ...addedElements, + ], backgrounds: backgrounds); + } else if (addsCombinedHighlight) { + await rendererCubit.unbake( + this, + blocState, + unbakedElements: [...rendererCubit.renderers, ...addedElements], + ); + } else if (unbake) { + await rendererCubit.unbake(this, blocState, backgrounds: backgrounds); + } else if (backgrounds != null) { + await rendererCubit.unbake(this, blocState, backgrounds: backgrounds); + } else { + await rendererCubit.addUnbaked(this, blocState, addedElements); + } + + saveCubit.setSaveState(saved: SaveState.unsaved); + if (saveCubit.state.embedding != null) { + return; + } + if (reset) { + await reload(bloc, current); + } else { + final refreshRequested = shouldRefresh?.call() == true; + if (refreshRequested) { + // For replacement updates we force a reset bake below, so avoid + // scheduling an extra delayed bake from refresh(). + await toolCubit.refresh( + this, + current, + allowBake: replacedElements == null, + ); + } + if (replacedElements != null) { + await rendererCubit.bake(this, blocState, reset: true); + } + } + if (updateIndex) { + toolCubit.updateIndex(this, bloc); + } + if (saveCubit.hasAutosave(networkingService)) { + saveCubit.save(bloc, networkingService, isAutosave: true); + } + } + + Future reloadTool( + DocumentBloc bloc, [ + DocumentLoaded? blocState, + ]) async { + final current = blocState ?? bloc.state; + if (current is! DocumentLoaded) return; + final tools = current.info.tools; + final toolIndex = toolCubit.state.index ?? 0; + final newTool = tools.elementAtOrNull(toolIndex); + if (newTool?.isAction() ?? true) { + await toolCubit.changeTool(this, bloc, index: 0, allowBake: false); + } else if (newTool != toolCubit.state.handler.data) { + await toolCubit.changeTool( + this, + bloc, + index: toolIndex, + allowBake: false, + ); + } + } + + Future reloadRuntime( + DocumentBloc bloc, [ + DocumentLoaded? blocState, + ]) async { + final current = blocState ?? bloc.state; + if (current is! DocumentLoaded) return; + await reloadTool(bloc, current); + await rendererCubit.loadElements(this, current); + await toolCubit.refresh(this, current, allowBake: false); + await rendererCubit.delayedBake(this, current); + } } diff --git a/app/lib/cubits/editor_controller_methods.dart b/app/lib/cubits/editor_controller_methods.dart deleted file mode 100644 index 15e6128dd187..000000000000 --- a/app/lib/cubits/editor_controller_methods.dart +++ /dev/null @@ -1,2425 +0,0 @@ -part of 'editor_controller.dart'; - -extension EditorControllerMethods on EditorController { - bool _sameRendererList( - List> a, - List> b, - ) { - if (identical(a, b)) return true; - if (a.length != b.length) return false; - for (var i = 0; i < a.length; i++) { - if (!identical(a[i], b[i])) return false; - } - return true; - } - - void _onTransformChanged(CameraTransform transform) { - // Debounce transform changes to avoid excessive updates during pan/zoom - _transformDebounceTimer?.cancel(); - _transformDebounceTimer = Timer(const Duration(milliseconds: 16), () { - _updateVisibleElements(); - }); - } - - void _updateVisibleElements() { - if (isClosed) return; - final unbaked = rendererCubit.state.cameraViewport.unbakedElements; - final baked = rendererCubit.state.cameraViewport.bakedElements; - - final rect = getViewportRect(); - final currentVisible = rendererCubit.state.cameraViewport.visibleElements; - final currentVisibleUnbaked = - rendererCubit.state.cameraViewport.visibleUnbakedElements; - - final visibleUnbaked = unbaked.where((e) => e.isVisible(rect)).toList(); - final visible = >[ - ...baked.where((e) => e.isVisible(rect)), - ...visibleUnbaked, - ]; - - if (_sameRendererList(visible, currentVisible) && - _sameRendererList(visibleUnbaked, currentVisibleUnbaked)) { - return; - } - - final newViewport = rendererCubit.state.cameraViewport.withUnbaked( - unbaked, - visibleElements: visible, - visibleUnbakedElements: visibleUnbaked, - ); - - final docState = _activeDocumentState; - if (docState != null) { - _updateOnVisible(newViewport, docState).then((_) { - final bloc = _activeDocumentBloc; - if (!isClosed && bloc != null) { - bloc.delayedBake(); - } - }); - } - - if (isClosed) return; - - rendererCubit.setViewport(newViewport); - } - - DocumentBloc? get _activeDocumentBloc { - final bloc = _documentBloc?.target; - if (bloc == null || bloc.isClosed) return null; - return bloc; - } - - DocumentLoaded? get _activeDocumentState { - final state = _activeDocumentBloc?.state; - return state is DocumentLoaded ? state : null; - } - - void init(DocumentBloc bloc) { - _documentBloc = WeakReference(bloc); - final blocState = bloc.state; - final index = blocState is DocumentLoadSuccess - ? editorSessionCubit?.resolveToolIndex(blocState.info) - : toolCubit.state.index; - changeTool(bloc, index: index ?? 0); - networkingService.setup(bloc); - } - - void invalidateRenderers(Iterable> renderers) { - rendererCubit.initializedElements.removeAll(renderers); - } - - Future _updateOnVisible( - CameraViewport newViewport, - DocumentLoaded blocState, { - CameraTransform? renderTransform, - ui.Size? targetSize, - }) async { - final newVisibleList = newViewport.visibleElements; - final nextVisibleSet = newVisibleList.toSet(); - - final newVisible = newVisibleList - .where((e) => !rendererCubit.initializedElements.contains(e)) - .toList(); - - final newlyHidden = rendererCubit.initializedElements - .where((e) => !nextVisibleSet.contains(e)) - .toList(); - - if (newVisible.isEmpty && newlyHidden.isEmpty) return; - - final transform = renderTransform ?? transformCubit.state; - final size = targetSize ?? newViewport.toSize(); - - rendererCubit.initializedElements.removeAll(newlyHidden); - - if (newVisible.isNotEmpty) { - talker.verbose('Updating visible elements: ${newVisible.length} new'); - final initialized = await Future.wait( - newVisible.map((element) async { - try { - await Future.sync( - () => element.onVisible(this, blocState, transform, size), - ); - return element; - } catch (error, stackTrace) { - talker.error( - 'Failed to initialize visible renderer $element', - error, - stackTrace, - ); - } - return null; - }), - ); - rendererCubit.initializedElements.addAll(initialized.nonNulls); - } - - if (newlyHidden.isNotEmpty) { - await Future.wait( - newlyHidden.map( - (element) async => - await element.onHidden(this, blocState, transform, size), - ), - ); - } - } - - Handler getHandler({bool disableTemporary = false}) { - if (saveCubit.state.embedding?.editable == false) { - return HandHandler(); - } - if (disableTemporary) { - return toolCubit.state.handler; - } else { - return toolCubit.state.temporaryHandler ?? toolCubit.state.handler; - } - } - - Future changeTool( - DocumentBloc bloc, { - int? index, - BuildContext? context, - Handler? handler, - bool allowBake = true, - }) async { - talker.verbose('Changing tool to index: $index'); - await toolCubit.resetInput(bloc, inputCubit); - final blocState = bloc.state; - if (blocState is! DocumentLoadSuccess) return null; - if (saveCubit.state.embedding?.editable == false) { - return null; - } - final document = blocState.data; - final info = blocState.info; - index ??= toolCubit.state.index ?? 0; - if (handler == null && (index < 0 || index >= info.tools.length)) { - return null; - } - handler ??= Handler.fromTool(info.tools[index]); - var selectState = SelectState.normal; - if (context != null) { - selectState = await handler.onSelected(context); - } - if (selectState != SelectState.none) { - toolCubit.state.handler.dispose(bloc); - toolCubit.state.temporaryHandler?.dispose(bloc); - _disposeTemporaryForegrounds(); - _disposeForegrounds(); - final foregrounds = handler.createForegrounds( - this, - document, - blocState.page, - info, - blocState.currentArea, - ); - if (handler.setupForegrounds) { - await Future.wait( - foregrounds.map( - (e) async => await e.setup( - transformCubit, - document, - blocState.assetService, - blocState.page, - ), - ), - ); - } - if (selectState == SelectState.normal) { - editorSessionCubit?.updateSelectedTool(handler.data, index); - toolCubit.setActiveTool( - index: index, - handler: handler, - cursor: handler.cursor ?? MouseCursor.defer, - foregrounds: foregrounds, - toolbar: await handler.getToolbar(bloc), - rendererStates: handler.rendererStates, - ); - rendererCubit.setRendererStates( - rendererStates: handler.rendererStates, - temporaryRendererStates: const {}, - ); - if (allowBake) await bake(blocState); - } else { - if (isHandlerEnabled(index)) { - disableHandler(bloc, index); - } else { - toolCubit.setToggleable( - handlers: {...toolCubit.state.toggleableHandlers, index: handler}, - foregrounds: { - ...toolCubit.state.toggleableForegrounds, - index: foregrounds, - }, - ); - } - } - } - return handler; - } - - void _onToolChanged(ToolRuntimeState next) { - if (_isClosing) { - return; - } - final current = _previousToolState; - _previousToolState = next; - if (current == null) return; - - if (next.foregrounds != current.foregrounds || - next.temporaryForegrounds != current.temporaryForegrounds) { - _networkingDebounceTimer?.cancel(); - _networkingDebounceTimer = Timer(const Duration(milliseconds: 50), () { - if (!isClosed) _sendNetworkingState(); - }); - } - } - - void _onInputChanged(EditorInputState next) { - if (_isClosing) return; - final current = _previousInputState; - _previousInputState = next; - if (next.lastPosition != current.lastPosition) { - _networkingDebounceTimer?.cancel(); - _networkingDebounceTimer = Timer(const Duration(milliseconds: 50), () { - if (!isClosed) _sendNetworkingState(); - }); - } - } - - void _onViewChanged(EditorViewState next) { - if (_isClosing) return; - final current = _previousViewState; - _previousViewState = next; - if (current != null && next.userName != current.userName) { - _networkingDebounceTimer?.cancel(); - _networkingDebounceTimer = Timer(const Duration(milliseconds: 50), () { - if (!isClosed) _sendNetworkingState(); - }); - } - } - - void _onRendererChanged(RendererRuntimeState next) { - if (_isClosing) return; - final current = _previousRendererState; - _previousRendererState = next; - final currentViewport = current.cameraViewport; - final newViewport = next.cameraViewport; - - if (!identical(currentViewport, newViewport) && - currentViewport != newViewport) { - toolCubit.state.handler.onViewportUpdated(currentViewport, newViewport); - toolCubit.state.temporaryHandler?.onViewportUpdated( - currentViewport, - newViewport, - ); - } - - currentViewport.disposeImages(except: newViewport); - } - - void _sendNetworkingState({ - List>? foregrounds, - Offset? cursor, - }) { - cursor ??= inputCubit.state.lastPosition ?? Offset.zero; - networkingService.sendUser( - NetworkingUser( - cursor: transformCubit.state.localToGlobal(cursor).toPoint(), - foreground: (foregrounds ?? toolCubit.state.getAllForegrounds(false)) - .map((e) => e.element) - .whereType() - .toList(), - name: networkingService.userName, - ), - ); - } - - Future updateNetworkingState( - DocumentBloc bloc, [ - Map? current, - ]) async { - talker.verbose('Updating networking state'); - final blocState = bloc.state; - if (blocState is! DocumentLoadSuccess) return; - final users = (current ?? networkingService.users).entries.toList(); - final usersByChannel = {for (final entry in users) entry.key: entry.value}; - final activeForegroundElements = users - .expand((entry) => entry.value.foreground ?? const []) - .toSet(); - - final foregrounds = toolCubit.state.networkingForegrounds.toList(); - foregrounds.removeWhere((renderer) { - bool shouldRemove; - if (renderer is UserCursor) { - final activeUser = usersByChannel[renderer.userId]; - shouldRemove = activeUser == null || activeUser != renderer.element; - } else { - shouldRemove = !activeForegroundElements.contains(renderer.element); - } - if (shouldRemove) { - renderer.dispose(); - } - return shouldRemove; - }); - - final existingElements = foregrounds.map((e) => e.element).toSet(); - final added = []; - added.addAll( - users - .expand((entry) => entry.value.foreground ?? const []) - .where((element) => !existingElements.contains(element)) - .map((element) => Renderer.fromInstance(element)), - ); - added.addAll( - users - .where( - (entry) => - foregrounds.whereType().firstWhereOrNull( - (cursor) => - cursor.userId == entry.key && - cursor.element == entry.value, - ) == - null, - ) - .map((entry) => UserCursor(entry.value, entry.key)), - ); - await Future.wait( - added.map( - (e) async => await e.setup( - transformCubit, - blocState.data, - blocState.assetService, - blocState.page, - ), - ), - ); - foregrounds.addAll(added); - toolCubit.setForegrounds(networkingForegrounds: foregrounds); - } - - void updateLastPosition(Offset position) { - // Only emit if position changed by more than 1 pixel to reduce state updates - final lastPos = inputCubit.state.lastPosition; - if (lastPos != null) { - final dx = (position.dx - lastPos.dx).abs(); - final dy = (position.dy - lastPos.dy).abs(); - if (dx < 1 && dy < 1) return; - } - inputCubit.updateLastPosition(position); - } - - Future updateHandler(DocumentBloc bloc, Handler handler) async { - toolCubit.replace( - toolCubit.state.copyWith( - handler: handler, - cursor: handler.cursor ?? MouseCursor.defer, - toolbar: await handler.getToolbar(bloc), - ), - ); - rendererCubit.setRendererStates(rendererStates: handler.rendererStates); - } - - Future updateTool(DocumentBloc bloc, Tool tool) async { - talker.verbose('Updating tool: ${tool.runtimeType}'); - final docState = bloc.state; - if (docState is! DocumentLoadSuccess) return; - toolCubit.state.handler.dispose(bloc); - final handler = Handler.fromTool(tool); - _disposeForegrounds(); - final foregrounds = handler.createForegrounds( - this, - docState.data, - docState.page, - docState.info, - docState.currentArea, - ); - if (handler.setupForegrounds) { - await Future.wait( - foregrounds.map( - (e) async => await e.setup( - transformCubit, - docState.data, - docState.assetService, - docState.page, - ), - ), - ); - } - toolCubit.setActiveTool( - index: toolCubit.state.index, - handler: handler, - cursor: handler.cursor ?? MouseCursor.defer, - foregrounds: foregrounds, - toolbar: await handler.getToolbar(bloc), - rendererStates: handler.rendererStates, - ); - rendererCubit.setRendererStates(rendererStates: handler.rendererStates); - } - - Future updateTemporaryTool(DocumentBloc bloc, Tool tool) async { - talker.verbose('Updating temporary tool: ${tool.runtimeType}'); - final docState = bloc.state; - if (docState is! DocumentLoadSuccess) return; - toolCubit.state.temporaryHandler?.dispose(bloc); - final handler = Handler.fromTool(tool); - _disposeTemporaryForegrounds(); - final foregrounds = handler.createForegrounds( - this, - docState.data, - docState.page, - docState.info, - docState.currentArea, - ); - if (handler.setupForegrounds) { - await Future.wait( - foregrounds.map( - (e) async => await e.setup( - transformCubit, - docState.data, - docState.assetService, - docState.page, - ), - ), - ); - } - toolCubit.setTemporaryTool( - handler: handler, - index: toolCubit.state.temporaryIndex, - foregrounds: foregrounds, - toolbar: await handler.getToolbar(bloc), - cursor: handler.cursor, - rendererStates: handler.rendererStates, - ); - rendererCubit.setRendererStates( - temporaryRendererStates: handler.rendererStates, - ); - } - - T? fetchHandler({bool disableTemporary = false}) { - final handler = getHandler(disableTemporary: disableTemporary); - if (handler is T) return handler; - return null; - } - - void _disposeForegrounds() { - for (final r in toolCubit.state.foregrounds) { - r.dispose(); - } - } - - void _disposeTemporaryForegrounds() { - for (final r in toolCubit.state.temporaryForegrounds ?? []) { - r.dispose(); - } - } - - void _disposeNetworkingForegrounds() { - for (final r in toolCubit.state.networkingForegrounds) { - r.dispose(); - } - } - - void _disposeToggleableForegrounds() { - for (final r in toolCubit.state.toggleableForegrounds.values.expand( - (e) => e, - )) { - r.dispose(); - } - } - - void _disposeAllForegrounds() { - _disposeForegrounds(); - _disposeTemporaryForegrounds(); - _disposeNetworkingForegrounds(); - _disposeToggleableForegrounds(); - } - - R useHandler( - DocumentBloc bloc, - int index, - R Function(Handler handler) callback, - ) { - Handler? handler; - bool needsDispose = false; - if (toolCubit.state.index == index) { - handler = fetchHandler>(disableTemporary: true); - } else if (toolCubit.state.toggleableHandlers.containsKey(index)) { - handler = toolCubit.state.toggleableHandlers[index]; - } - if (handler == null) { - List tools = const []; - final blocState = bloc.state; - if (blocState is DocumentLoaded) tools = blocState.info.tools; - final tool = tools.elementAtOrNull(index) ?? HandTool(); - handler = Handler.fromTool(tool); - needsDispose = true; - } - final result = callback(handler); - if (needsDispose) { - if (result is Future) { - result.then((value) => handler?.dispose(bloc)); - } else { - handler.dispose(bloc); - } - } - return result; - } - - Future refresh( - DocumentLoaded blocState, { - bool allowBake = true, - }) async { - talker.verbose('Refreshing EditorController'); - final document = blocState.data; - final page = blocState.page; - final info = blocState.info; - final assetService = blocState.assetService; - final currentArea = blocState.currentArea; - const mapEq = MapEquality(); - if (!isClosed) { - _disposeAllForegrounds(); - final temporaryForegrounds = toolCubit.state.temporaryHandler - ?.createForegrounds(this, document, page, info, currentArea); - if (temporaryForegrounds != null && - toolCubit.state.temporaryHandler?.setupForegrounds == true) { - await Future.wait( - temporaryForegrounds.map( - (e) async => - await e.setup(transformCubit, document, assetService, page), - ), - ); - } - final foregrounds = toolCubit.state.handler.createForegrounds( - this, - document, - page, - info, - currentArea, - ); - if (toolCubit.state.handler.setupForegrounds) { - await Future.wait( - foregrounds.map( - (e) async => - await e.setup(transformCubit, document, assetService, page), - ), - ); - } - final toggleableForegrounds = >{}; - for (final entry in toolCubit.state.toggleableHandlers.entries) { - final handler = entry.value; - final index = entry.key; - final foregrounds = handler.createForegrounds( - this, - document, - page, - info, - currentArea, - ); - if (handler.setupForegrounds) { - await Future.wait( - foregrounds.map( - (e) async => - await e.setup(transformCubit, document, assetService, page), - ), - ); - } - toggleableForegrounds[index] = foregrounds; - } - final rendererStates = toolCubit.state.handler.rendererStates; - final temporaryRendererStates = - toolCubit.state.temporaryHandler?.rendererStates; - final statesChanged = !mapEq.equals( - rendererCubit.state.rendererStates, - rendererStates, - ); - final temporaryStatesChanged = !mapEq.equals( - rendererCubit.state.temporaryRendererStates, - temporaryRendererStates, - ); - final shouldBake = statesChanged || temporaryStatesChanged; - toolCubit.setForegrounds( - temporaryForegrounds: temporaryForegrounds, - toggleableForegrounds: toggleableForegrounds, - foregrounds: foregrounds, - cursor: toolCubit.state.handler.cursor ?? MouseCursor.defer, - temporaryCursor: toolCubit.state.temporaryHandler?.cursor, - rendererStates: statesChanged - ? rendererStates - : rendererCubit.state.rendererStates, - temporaryRendererStates: temporaryStatesChanged - ? temporaryRendererStates - : rendererCubit.state.temporaryRendererStates, - ); - rendererCubit.setRendererStates( - rendererStates: statesChanged - ? rendererStates - : rendererCubit.state.rendererStates, - temporaryRendererStates: temporaryStatesChanged - ? temporaryRendererStates - : rendererCubit.state.temporaryRendererStates, - ); - if (allowBake) { - if (shouldBake) { - return bake(blocState, reset: true); - } else if (!rendererCubit.state.cameraViewport.baked) { - return delayedBake(blocState); - } - } - } - } - - Future refreshToolbar(DocumentBloc bloc) async { - if (!isClosed) { - final toolbar = await toolCubit.state.handler.getToolbar(bloc); - final temporaryToolbar = await toolCubit.state.temporaryHandler - ?.getToolbar(bloc); - toolCubit.setToolbar( - toolbar: toolbar, - temporaryToolbar: temporaryToolbar, - ); - } - } - - /// Lightweight refresh that only updates foregrounds without rebaking. - /// Use this when handler internal state changes but document hasn't changed. - Future refreshForegrounds(DocumentLoaded blocState) => toolCubit - .foregroundRefreshRunner - .schedule(() => _refreshForegrounds(blocState)); - - Future _refreshForegrounds(DocumentLoaded blocState) async { - if (isClosed) return; - final document = blocState.data; - final page = blocState.page; - final info = blocState.info; - final assetService = blocState.assetService; - final currentArea = blocState.currentArea; - - _disposeForegrounds(); - _disposeTemporaryForegrounds(); - - final temporaryForegrounds = toolCubit.state.temporaryHandler - ?.createForegrounds(this, document, page, info, currentArea); - if (temporaryForegrounds != null && - temporaryForegrounds.isNotEmpty && - toolCubit.state.temporaryHandler?.setupForegrounds == true) { - await Future.wait( - temporaryForegrounds.map( - (e) async => - await e.setup(transformCubit, document, assetService, page), - ), - ); - } - - final foregrounds = toolCubit.state.handler.createForegrounds( - this, - document, - page, - info, - currentArea, - ); - if (foregrounds.isNotEmpty && toolCubit.state.handler.setupForegrounds) { - await Future.wait( - foregrounds.map( - (e) async => - await e.setup(transformCubit, document, assetService, page), - ), - ); - } - - // Check if rendererStates changed and need a bake - const mapEq = MapEquality(); - final rendererStates = toolCubit.state.handler.rendererStates; - final temporaryRendererStates = - toolCubit.state.temporaryHandler?.rendererStates; - final statesChanged = !mapEq.equals( - rendererCubit.state.rendererStates, - rendererStates, - ); - final temporaryStatesChanged = !mapEq.equals( - rendererCubit.state.temporaryRendererStates, - temporaryRendererStates, - ); - - toolCubit.setForegrounds( - foregrounds: foregrounds, - temporaryForegrounds: temporaryForegrounds, - cursor: toolCubit.state.handler.cursor ?? MouseCursor.defer, - temporaryCursor: toolCubit.state.temporaryHandler?.cursor, - rendererStates: statesChanged - ? rendererStates - : rendererCubit.state.rendererStates, - temporaryRendererStates: temporaryStatesChanged - ? temporaryRendererStates - : rendererCubit.state.temporaryRendererStates, - ); - rendererCubit.setRendererStates( - rendererStates: statesChanged - ? rendererStates - : rendererCubit.state.rendererStates, - temporaryRendererStates: temporaryStatesChanged - ? temporaryRendererStates - : rendererCubit.state.temporaryRendererStates, - ); - - // If renderer states changed, we need to bake to hide/show original elements - if (statesChanged || temporaryStatesChanged) { - await bake(blocState, reset: true); - } - } - - /// Ultra-lightweight update for cursor changes only. - /// Use this when only the cursor appearance needs to change. - void updateCursor(MouseCursor cursor) { - if (toolCubit.state.cursor != cursor) { - toolCubit.setCursor(cursor); - } - } - - Future toggleHandler(DocumentBloc bloc, int index) async { - if (toolCubit.state.toggleableHandlers.containsKey(index)) { - disableHandler(bloc, index); - } else { - await enableHandler(bloc, index); - } - } - - Future enableHandler(DocumentBloc bloc, int index) async { - final blocState = bloc.state; - if (blocState is! DocumentLoaded) return null; - if (index < 0 || index >= blocState.info.tools.length) { - return null; - } - final tool = blocState.info.tools[index]; - final handler = Handler.fromTool(tool); - final document = blocState.data; - final page = blocState.page; - final info = blocState.info; - final currentArea = blocState.currentArea; - final foregrounds = handler.createForegrounds( - this, - document, - page, - info, - currentArea, - ); - if (handler.setupForegrounds) { - await Future.wait( - foregrounds.map( - (e) async => await e.setup( - transformCubit, - document, - blocState.assetService, - page, - ), - ), - ); - } - toolCubit.setToggleable( - handlers: Map.from(toolCubit.state.toggleableHandlers)..[index] = handler, - foregrounds: Map.from(toolCubit.state.toggleableForegrounds) - ..[index] = foregrounds, - ); - return handler; - } - - bool disableHandler(DocumentBloc bloc, int index) { - final handler = toolCubit.state.toggleableHandlers[index]; - if (handler == null) { - return false; - } - handler.dispose(bloc); - final foregrounds = Map>.from( - toolCubit.state.toggleableForegrounds, - ); - final current = foregrounds.remove(index); - for (final r in current ?? []) { - r.dispose(); - } - toolCubit.setToggleable( - handlers: Map.from(toolCubit.state.toggleableHandlers)..remove(index), - foregrounds: foregrounds, - ); - return true; - } - - bool isHandlerEnabled(int index) => - toolCubit.state.toggleableHandlers.containsKey(index); - - void reset(DocumentBloc bloc) { - for (final r in rendererCubit.renderers) { - r.dispose(); - } - rendererCubit.initializedElements.clear(); - toolCubit.state.handler.dispose(bloc); - toolCubit.state.temporaryHandler?.dispose(bloc); - for (var e in toolCubit.state.toggleableHandlers.values) { - e.dispose(bloc); - } - _disposeForegrounds(); - _disposeTemporaryForegrounds(); - _disposeNetworkingForegrounds(); - _disposeToggleableForegrounds(); - toolCubit.resetRuntime(); - rendererCubit.replace(const RendererRuntimeState()); - } - - Future changeTemporaryHandlerIndex( - BuildContext context, - int index, { - DocumentBloc? bloc, - TemporaryState temporaryState = TemporaryState.allowClick, - bool force = false, - }) async { - bloc ??= context.read(); - final blocState = bloc.state; - if (blocState is! DocumentLoadSuccess) return null; - if (index < 0 || index >= blocState.info.tools.length) { - return null; - } - final tool = blocState.info.tools[index]; - final temporaryHandler = toolCubit.state.temporaryHandler; - if (!force && - index == toolCubit.state.temporaryIndex && - temporaryHandler != null) { - return temporaryHandler; - } - return changeTemporaryHandler( - context, - tool, - bloc: bloc, - temporaryState: temporaryState, - index: index, - ); - } - - Future?> changeTemporaryHandler( - BuildContext context, - T tool, { - DocumentBloc? bloc, - int? index, - TemporaryState temporaryState = TemporaryState.allowClick, - }) async { - bloc ??= context.read(); - final handler = Handler.fromTool(tool); - final blocState = bloc.state; - if (blocState is! DocumentLoadSuccess) return null; - final document = blocState.data; - final page = blocState.page; - final currentArea = blocState.currentArea; - toolCubit.state.temporaryHandler?.dispose(bloc); - final selectState = await handler.onSelected(context); - - if (selectState == SelectState.normal) { - _disposeTemporaryForegrounds(); - final temporaryForegrounds = handler.createForegrounds( - this, - document, - page, - blocState.info, - currentArea, - ); - if (handler.setupForegrounds) { - await Future.wait( - temporaryForegrounds.map( - (e) async => await e.setup( - transformCubit, - document, - blocState.assetService, - page, - ), - ), - ); - } - toolCubit.setTemporaryTool( - handler: handler, - index: index, - foregrounds: temporaryForegrounds, - toolbar: await handler.getToolbar(bloc), - cursor: handler.cursor, - rendererStates: handler.rendererStates, - temporaryState: temporaryState, - ); - rendererCubit.setRendererStates( - temporaryRendererStates: handler.rendererStates, - ); - await bake(blocState); - } else if (selectState == SelectState.toggle && index != null) { - await toggleHandler(bloc, index); - } - return handler; - } - - void resetReleaseHandler(DocumentBloc bloc) { - if (toolCubit.state.temporaryState == TemporaryState.removeAfterRelease) { - resetTemporaryHandler(bloc, true); - } - } - - void resetDownHandler(DocumentBloc bloc) { - resetTemporaryHandler(bloc); - } - - void resetTemporaryHandler(DocumentBloc bloc, [bool force = false]) { - if (toolCubit.state.temporaryHandler == null) { - return; - } - if (!force && - toolCubit.state.temporaryState != TemporaryState.removeAfterClick) { - if (toolCubit.state.temporaryState == TemporaryState.allowClick) { - toolCubit.setTemporaryState(TemporaryState.removeAfterClick); - } - return; - } - toolCubit.state.temporaryHandler?.dispose(bloc); - _disposeTemporaryForegrounds(); - toolCubit.setTemporaryTool( - handler: null, - index: null, - foregrounds: null, - toolbar: null, - cursor: null, - rendererStates: null, - ); - rendererCubit.setRendererStates(temporaryRendererStates: const {}); - } - - Renderer? getRenderer(PadElement element) => - rendererCubit.getRenderer(element); - - Rect getViewportRect({Size? viewportSize}) { - var size = viewportSize ?? rendererCubit.state.cameraViewport.toSize(); - - final transform = transformCubit.state; - final resolution = settingsCubit.state.renderResolution; - - final friction = transform.friction; - final realWidth = size.width / transform.size; - final realHeight = size.height / transform.size; - Rect rect = Rect.fromLTWH( - transform.position.dx, - transform.position.dy, - realWidth, - realHeight, - ); - if (friction != null) { - final beginPosition = transform.position - friction.beginOffset; - final topLeft = Offset( - min(transform.position.dx, beginPosition.dx), - min(transform.position.dy, beginPosition.dy), - ); - final frictionSize = Size( - realWidth + (friction.beginOffset.dx * transform.size).abs(), - realHeight + (friction.beginOffset.dy * transform.size).abs(), - ); - rect = topLeft & frictionSize; - } - return _snapViewportRect(rect, size, transform, resolution); - } - - Rect _snapViewportRect( - Rect rect, - Size size, - CameraTransform transform, - RenderResolution resolution, - ) { - final screenRect = Rect.fromPoints( - transform.globalToLocal(rect.topLeft), - transform.globalToLocal(rect.bottomRight), - ); - final snappedRect = _expandScreenRect( - Rect.fromLTRB( - screenRect.left.floorToDouble(), - screenRect.top.floorToDouble(), - screenRect.right.ceilToDouble(), - screenRect.bottom.ceilToDouble(), - ), - Size( - (size.width * resolution.multiplier).ceilToDouble(), - (size.height * resolution.multiplier).ceilToDouble(), - ), - ); - return Rect.fromPoints( - transform.localToGlobal(snappedRect.topLeft), - transform.localToGlobal(snappedRect.bottomRight), - ); - } - - Rect _expandScreenRect(Rect rect, Size minimumSize) { - final dx = max(0.0, minimumSize.width - rect.width); - final dy = max(0.0, minimumSize.height - rect.height); - return Rect.fromLTRB( - rect.left - (dx / 2).floorToDouble(), - rect.top - (dy / 2).floorToDouble(), - rect.right + (dx / 2).ceilToDouble(), - rect.bottom + (dy / 2).ceilToDouble(), - ); - } - - bool _rectContains(Rect outer, Rect inner) { - const tolerance = precisionErrorTolerance; - return outer.left <= inner.left + tolerance && - outer.top <= inner.top + tolerance && - outer.right >= inner.right - tolerance && - outer.bottom >= inner.bottom - tolerance; - } - - Future bake( - DocumentLoaded blocState, { - Size? viewportSize, - double? pixelRatio, - bool reset = false, - bool resetAllLayers = false, - }) => rendererCubit.bakeLock.synchronized(() async { - if (isClosed) return; - var cameraViewport = rendererCubit.state.cameraViewport; - final startTransform = transformCubit.state; - final startViewport = cameraViewport; - final resolution = settingsCubit.state.renderResolution; - var size = viewportSize ?? cameraViewport.toSize(); - final ratio = pixelRatio ?? cameraViewport.pixelRatio; - if (size.height <= 0 || size.width <= 0) { - return; - } - if (viewportSize == null) { - size /= resolution.multiplier; - } - var transform = transformCubit.state; - var renderers = List>.from(rendererCubit.renderers); - final recorder = ui.PictureRecorder(); - final canvas = ui.Canvas(recorder); - final rect = getViewportRect(viewportSize: size); - size = rect.size * transform.size; - final renderTransform = transform.improve(resolution, rect); - final document = blocState.data; - final page = blocState.page; - final info = blocState.info; - final imageWidth = (size.width * ratio).ceil(); - final imageHeight = (size.height * ratio).ceil(); - var allRendererStates = rendererCubit.state.allRendererStates; - final rendererStatesChanged = !mapEquals( - allRendererStates, - cameraViewport.rendererStates, - ); - if (!rendererStatesChanged) { - allRendererStates = cameraViewport.rendererStates; - } - final invisibleLayers = blocState.invisibleLayers; - final viewportAlreadyCoversRect = - cameraViewport.image != null && - cameraViewport.scale == transform.size && - cameraViewport.resolution == resolution && - cameraViewport.pixelRatio == ratio && - !rendererStatesChanged && - setEquals(cameraViewport.invisibleLayers, invisibleLayers) && - _rectContains(cameraViewport.toRect(), rect); - final viewChanged = - !viewportAlreadyCoversRect && - (cameraViewport.width != size.width.ceil() || - cameraViewport.height != size.height.ceil() || - cameraViewport.pixelRatio != ratio || - cameraViewport.resolution != resolution || - cameraViewport.x != renderTransform.position.dx || - cameraViewport.y != renderTransform.position.dy || - cameraViewport.scale != transform.size || - rendererStatesChanged || - !setEquals(cameraViewport.invisibleLayers, invisibleLayers)); - reset = reset || viewChanged; - resetAllLayers = resetAllLayers || viewChanged; - if (cameraViewport.unbakedElements.isEmpty && !reset) return; - final currentLayer = blocState.currentLayer; - List> visibleElements; - final oldVisible = cameraViewport.visibleElements; - final oldVisibleSet = oldVisible.toSet(); - talker.verbose( - 'Baking viewport (reset: $reset, viewChanged: $viewChanged, ' - 'rendererStatesChanged: $rendererStatesChanged)', - ); - - if (reset) { - visibleElements = renderers - .where((renderer) => renderer.isVisible(rect)) - .toList(); - } else { - visibleElements = List.from(oldVisible) - ..addAll( - cameraViewport.unbakedElements.where( - (renderer) => - !oldVisibleSet.contains(renderer) && renderer.isVisible(rect), - ), - ); - } - - final visibleElementsSet = visibleElements.toSet(); - - await _updateOnVisible( - cameraViewport.unbake(visibleElements: visibleElements), - blocState, - renderTransform: renderTransform, - targetSize: size, - ); - - canvas.scale(ratio); - - if (viewChanged && visibleElements.isNotEmpty) { - await Future.wait( - visibleElements.map( - (e) async => - await e.updateView(this, blocState, renderTransform, size), - ), - ); - } - - // Wait one frame - await Future.delayed(const Duration(milliseconds: 1)); - - ViewPainter( - document, - page, - info, - transform: renderTransform, - cameraViewport: reset - ? cameraViewport.unbake( - rendererStates: allRendererStates, - unbakedElements: visibleElements - .where((e) => currentLayer == e.layer) - .toList(), - visibleElements: visibleElements, - ) - : cameraViewport, - renderBackground: false, - renderBaked: !reset, - renderBakedLayers: false, - invisibleLayers: invisibleLayers, - ).paint(canvas, size); - - final picture = recorder.endRecording(); - ui.Image newImage; - try { - newImage = await picture.toImage(imageWidth, imageHeight); - } finally { - picture.dispose(); - } - - var belowLayerImage = cameraViewport.belowLayerImage; - var aboveLayerImage = cameraViewport.aboveLayerImage; - - if (resetAllLayers) { - final belowLayerRecorder = ui.PictureRecorder(); - final belowLayerCanvas = ui.Canvas(belowLayerRecorder); - belowLayerCanvas.scale(ratio); - final aboveLayerRecorder = ui.PictureRecorder(); - final aboveLayerCanvas = ui.Canvas(aboveLayerRecorder); - aboveLayerCanvas.scale(ratio); - final belowLayers = {}, aboveLayers = {}; - bool above = false; - for (final layer in page.layers) { - if (layer.id == currentLayer) { - above = true; - continue; - } - final layerId = layer.id; - if (layerId == null) continue; - if (above) { - aboveLayers.add(layerId); - } else { - belowLayers.add(layerId); - } - } - - ViewPainter( - document, - page, - info, - transform: renderTransform, - cameraViewport: cameraViewport.unbake( - rendererStates: allRendererStates, - unbakedElements: visibleElements - .where((e) => e.layer != null && belowLayers.contains(e.layer)) - .toList(), - visibleElements: visibleElements, - ), - renderBackground: false, - renderBaked: false, - invisibleLayers: invisibleLayers, - ).paint(belowLayerCanvas, size); - ViewPainter( - document, - page, - info, - transform: renderTransform, - cameraViewport: cameraViewport.unbake( - rendererStates: allRendererStates, - unbakedElements: visibleElements - .where((e) => e.layer != null && aboveLayers.contains(e.layer)) - .toList(), - visibleElements: visibleElements, - ), - renderBackground: false, - renderBaked: false, - invisibleLayers: invisibleLayers, - ).paint(aboveLayerCanvas, size); - - final belowPicture = belowLayerRecorder.endRecording(); - final abovePicture = aboveLayerRecorder.endRecording(); - try { - final result = await Future.wait([ - belowPicture.toImage(imageWidth, imageHeight), - abovePicture.toImage(imageWidth, imageHeight), - ]); - belowLayerImage = result[0]; - aboveLayerImage = result[1]; - } finally { - belowPicture.dispose(); - abovePicture.dispose(); - } - } - - final bakedElementsSet = cameraViewport.bakedElements - .map((e) => e.element) - .toSet(); - final unbakedElementsSet = cameraViewport.unbakedElements - .map((e) => e.element) - .toSet(); - - final newlyUnbaked = - (reset - ? rendererCubit.renderers - : rendererCubit.state.cameraViewport.unbakedElements) - .where( - (element) => - !bakedElementsSet.contains(element.element) && - !unbakedElementsSet.contains(element.element) && - !visibleElementsSet.contains(element), - ) - .toList(); - - if (isClosed) return; - - // If state changed while baking (e.g. fast move submitted a newer viewport), - // this bake output is stale and must not overwrite the latest viewport. - final currentViewport = rendererCubit.state.cameraViewport; - final currentTransform = transformCubit.state; - if (!identical(currentViewport, startViewport) || - currentTransform != startTransform) { - newImage.dispose(); - final oldBelow = startViewport.belowLayerImage; - final oldAbove = startViewport.aboveLayerImage; - if (!identical(belowLayerImage, oldBelow)) { - belowLayerImage?.dispose(); - } - if (!identical(aboveLayerImage, oldAbove)) { - aboveLayerImage?.dispose(); - } - Future.microtask(() async { - final latestState = _activeDocumentState; - if (latestState == null) return; - await bake( - latestState, - viewportSize: viewportSize, - pixelRatio: pixelRatio, - reset: reset, - resetAllLayers: resetAllLayers, - ); - }); - return; - } - - final newViewport = cameraViewport.bake( - height: size.height, - width: size.width, - pixelRatio: ratio, - resolution: resolution, - scale: transform.size, - x: renderTransform.position.dx, - y: renderTransform.position.dy, - image: newImage, - bakedElements: renderers, - unbakedElements: newlyUnbaked, - visibleElements: visibleElements, - visibleUnbakedElements: newlyUnbaked - .where((renderer) => renderer.isVisible(rect)) - .toList(), - belowLayerImage: belowLayerImage, - aboveLayerImage: aboveLayerImage, - rendererStates: allRendererStates, - invisibleLayers: invisibleLayers, - ); - rendererCubit.setViewport(newViewport); - }); - - Future renderImage( - NoteData document, - DocumentPage page, - DocumentInfo info, - ImageExportOptions options, { - CameraViewport? cameraViewport, - Set? invisibleLayers, - DocumentLoaded? docState, - }) async { - final realWidth = (options.width * options.quality).ceil(); - final realHeight = (options.height * options.quality).ceil(); - final realZoom = options.scale; - if (realWidth <= 0 || realHeight <= 0) { - return null; - } - final size = Size(options.width, options.height); - final recorder = ui.PictureRecorder(); - final canvas = Canvas(recorder); - canvas.scale(options.quality); - final viewport = - cameraViewport ?? - rendererCubit.state.cameraViewport.unbake( - unbakedElements: rendererCubit.renderers, - ); - final transform = CameraTransform( - options.quality, - Offset(options.x, options.y), - realZoom, - ); - final hiddenRenderers = >[]; - if (docState != null) { - final exportRect = Rect.fromLTWH( - options.x, - options.y, - options.width, - options.height, - ); - for (final renderer in viewport.unbakedElements) { - if (renderer.isVisible(exportRect)) { - final wasInitialized = rendererCubit.initializedElements.contains( - renderer, - ); - if (!wasInitialized) { - await renderer.onVisible(this, docState, transform, size); - hiddenRenderers.add(renderer); - } - } - } - } - final painter = ViewPainter( - document, - page, - info, - renderBackground: options.renderBackground, - invisibleLayers: invisibleLayers, - cameraViewport: viewport, - transform: transform, - ); - painter.paint(canvas, size); - for (final renderer in hiddenRenderers) { - await renderer.onHidden(this, docState!, transform, size); - } - final picture = recorder.endRecording(); - ui.Image? image; - try { - image = await picture.toImage(realWidth, realHeight); - } finally { - picture.dispose(); - } - return image; - } - - Future render( - NoteData document, - DocumentPage page, - DocumentInfo info, - ImageExportOptions options, { - CameraViewport? cameraViewport, - Set? invisibleLayers, - DocumentLoaded? docState, - }) async { - final image = await renderImage( - document, - page, - info, - options, - cameraViewport: cameraViewport, - invisibleLayers: invisibleLayers, - docState: docState, - ); - ByteData? bytes; - try { - bytes = await image?.toByteData(format: ui.ImageByteFormat.png); - } finally { - image?.dispose(); - } - return bytes; - } - - XmlDocument renderSVG( - NoteData document, - DocumentPage page, - SvgExportOptions options, { - Set? invisibleLayers, - }) { - final xml = XmlDocument(); - xml.createElement( - 'svg', - attributes: { - 'xmlns': 'http://www.w3.org/2000/svg', - 'xmlns:xlink': 'http://www.w3.org/1999/xlink', - 'version': '1.1', - 'width': '${options.width}px', - 'height': '${options.height}px', - 'viewBox': - '${options.x} ${options.y} ${options.width} ${options.height}', - }, - ); - - final rect = Rect.fromLTWH( - options.x, - options.y, - options.width.toDouble(), - options.height.toDouble(), - ); - if (options.renderBackground) { - for (final e in rendererCubit.state.cameraViewport.backgrounds) { - e.buildSvg(xml, document, page, rect); - } - } - for (var e in rendererCubit.renderers) { - if ((invisibleLayers?.contains(e.layer) ?? false) || !e.isVisible(rect)) { - continue; - } - e.buildSvg(xml, document, page, rect); - } - return xml; - } - - Future unbake( - DocumentLoaded blocState, { - List>? backgrounds, - List>? unbakedElements, - }) async { - final elementsToCheck = unbakedElements ?? rendererCubit.renderers; - final oldViewport = rendererCubit.state.cameraViewport; - final newViewport = oldViewport.unbake( - unbakedElements: unbakedElements, - visibleElements: elementsToCheck - .where((e) => e.isVisible(getViewportRect())) - .toList(), - backgrounds: backgrounds, - ); - await _updateOnVisible(newViewport, blocState); - rendererCubit.setViewport(newViewport); - } - - Future replaceUnbaked( - DocumentLoaded blocState, - List> unbakedElements, { - List>? backgrounds, - }) async { - final visibleElements = unbakedElements - .where((e) => e.isVisible(getViewportRect())) - .toList(); - final newViewport = rendererCubit.state.cameraViewport.replaceUnbaked( - unbakedElements, - visibleElements: visibleElements, - visibleUnbakedElements: visibleElements, - backgrounds: backgrounds, - ); - await _updateOnVisible(newViewport, blocState); - rendererCubit.setViewport(newViewport); - } - - Future loadElements( - DocumentState docState, { - bool reset = false, - }) async { - if (docState is! DocumentLoaded) return; - final document = docState.data; - final assetService = docState.assetService; - final page = docState.page; - var existing = rendererCubit.renderers; - if (reset) { - for (var e in existing) { - rendererCubit.initializedElements.remove(e); - e.dispose(); - } - existing = []; - } - final elements = page.layers - .where((e) => !docState.invisibleLayers.contains(e.id)) - .expand((l) => l.content.map((e) => (e, l.id))) - .toList(); - final elementKeys = elements - .map((element) => (element.$1, element.$2)) - .toSet(); - final existingByKey = { - for (final renderer in existing) - (renderer.element, renderer.layer): renderer, - }; - final reusable = >[]; - final reusableKeys = <(PadElement, String?)>{}; - for (final element in elements) { - final key = (element.$1, element.$2); - final renderer = existingByKey[key]; - if (renderer != null) { - reusable.add(renderer); - reusableKeys.add(key); - } - } - final dropped = existing - .where( - (renderer) => - !elementKeys.contains((renderer.element, renderer.layer)), - ) - .toList(); - for (final e in dropped) { - rendererCubit.initializedElements.remove(e); - e.dispose(); - } - final newRenderers = elements - .where((e) => !reusableKeys.contains((e.$1, e.$2))) - .map((e) => Renderer.fromInstance(e.$1, e.$2)) - .toList(); - await Future.wait( - newRenderers.map( - (e) async => - await e.setup(transformCubit, document, assetService, page), - ), - ); - // Build layer index map for O(1) lookups instead of O(n) indexOf calls - final layersList = page.layers.map((e) => e.id).toList(); - final layerIndexMap = {}; - for (var i = 0; i < layersList.length; i++) { - layerIndexMap[layersList[i]] = i; - } - - // Build element index map for O(1) lookups - final elementIndexMap = {}; - for (var i = 0; i < elements.length; i++) { - elementIndexMap[elements[i].$1] = i; - } - - final combined = [...reusable, ...newRenderers] - ..sort((a, b) { - final layerA = layerIndexMap[a.layer] ?? layersList.length; - final layerB = layerIndexMap[b.layer] ?? layersList.length; - if (layerA != layerB) return layerA.compareTo(layerB); - final indexA = elementIndexMap[a.element] ?? -1; - final indexB = elementIndexMap[b.element] ?? -1; - return indexA.compareTo(indexB); - }); - final backgrounds = page.backgrounds.map(Renderer.fromInstance).toList(); - await Future.wait( - backgrounds.map( - (e) async => - await e.setup(transformCubit, document, assetService, page), - ), - ); - final rect = getViewportRect(); - final visibleElements = combined.where((e) => e.isVisible(rect)).toList(); - final oldViewport = rendererCubit.state.cameraViewport; - final newViewport = oldViewport.unbake( - unbakedElements: combined, - visibleElements: visibleElements, - backgrounds: backgrounds, - ); - await _updateOnVisible(newViewport, docState); - saveCubit.setSaveState( - location: saveCubit.state.embedding?.location ?? saveCubit.state.location, - ); - rendererCubit.setViewport(newViewport); - } - - Future addUnbaked( - DocumentLoaded blocState, - List> unbakedElements, [ - List>? visibleElements, - ]) async { - final rect = getViewportRect(); - visibleElements ??= unbakedElements - .where((e) => e.isVisible(rect)) - .toList(); - final nextUnbaked = [ - ...rendererCubit.state.cameraViewport.unbakedElements, - ...unbakedElements, - ]; - final newViewport = rendererCubit.state.cameraViewport.withUnbaked( - nextUnbaked, - visibleElements: [ - ...rendererCubit.state.cameraViewport.visibleElements, - ...visibleElements, - ], - visibleUnbakedElements: [ - ...rendererCubit.state.cameraViewport.visibleUnbakedElements, - ...visibleElements, - ], - ); - await _updateOnVisible(newViewport, blocState); - rendererCubit.setViewport(newViewport); - } - - void setSaveState({ - AssetLocation? location, - SaveState? saved, - bool absolute = false, - bool? isCreating, - bool keepRead = false, - }) => saveCubit.setSaveState( - location: location, - saved: saved, - absolute: absolute, - isCreating: isCreating, - keepRead: keepRead, - ); - - Future renderPDF( - DocumentLoaded docState, { - required List areas, - bool renderBackground = true, - void Function(double progress)? onProgress, - Set? invisibleLayers, - }) async { - var name = docState.metadata.name; - if (name.isEmpty) { - name = 'document'; - } - final pdf = await PdfDocument.createNew(sourceName: '$name.pdf'); - final document = docState.data; - final info = docState.info; - final pages = []; - final documents = []; - for (var i = 0; i < areas.length; i++) { - onProgress?.call(i / areas.length); - final preset = areas[i]; - final areaName = preset.name; - final quality = preset.quality; - final currentOpened = docState.pageName == preset.page; - final page = currentOpened - ? docState.page - : document.getPage(preset.page); - final area = preset.area ?? page?.getAreaByName(areaName); - if (area == null || page == null) { - continue; - } - final image = await renderImage( - document, - page, - info, - ImageExportOptions( - width: area.width, - height: area.height, - x: area.position.x, - y: area.position.y, - quality: quality, - renderBackground: renderBackground, - ), - cameraViewport: await CameraViewport.build( - transformCubit, - document, - docState.assetService, - page, - ), - docState: docState, - invisibleLayers: invisibleLayers ?? docState.invisibleLayers, - ); - if (image == null) continue; - final imgImage = await convertFlutterUiToImage(image); - final pdfImage = await compute( - (image) => img.JpegEncoder().encode(image), - imgImage, - ); - final imageDoc = await PdfDocument.createFromJpegData( - pdfImage, - width: area.width, - height: area.height, - sourceName: '$name-$areaName.jpg', - ); - pages.addAll(imageDoc.pages); - image.dispose(); - documents.add(imageDoc); - } - onProgress?.call(1.0); - pdf.pages = pages; - final bytes = await pdf.encodePdf(); - pdf.dispose(); - for (final doc in documents) { - doc.dispose(); - } - return bytes; - } - - void updateIndex(DocumentBloc bloc) { - final docState = bloc.state; - if (docState is! DocumentLoadSuccess) return; - final info = docState.info; - final index = info.tools.indexOf(toolCubit.state.handler.data); - if (index < 0) { - changeTool(bloc, index: toolCubit.state.index ?? 0); - } - if (index == toolCubit.state.index) { - return; - } - toolCubit.setIndex(index); - final selection = toolCubit.state.selection; - if (selection?.selected.contains(toolCubit.state.handler.data) ?? false) { - toolCubit.resetSelection(); - } - } - - Rect getContentRect([Area? currentArea]) { - if (currentArea != null) { - return currentArea.rect; - } - final renderers = rendererCubit.renderers; - if (renderers.isEmpty) { - return Rect.zero; - } - - var minX = double.infinity; - var minY = double.infinity; - var maxX = double.negativeInfinity; - var maxY = double.negativeInfinity; - - for (final renderer in rendererCubit.renderers) { - final rect = renderer.expandedRect; - if (rect != null) { - minX = min(minX, rect.left); - minY = min(minY, rect.top); - maxX = max(maxX, rect.right); - maxY = max(maxY, rect.bottom); - } - } - - if (minX == double.infinity) { - return Rect.zero; - } - - return Rect.fromLTRB(minX, minY, maxX, maxY); - } - - CameraTransform _clampTransform(CameraTransform transform) { - final bounds = _calculateViewportBounds(null, transform); - if (bounds == null) return transform; - return transform.withPosition( - Offset( - transform.position.dx.clamp(bounds.left, bounds.right), - transform.position.dy.clamp(bounds.top, bounds.bottom), - ), - ); - } - - bool _isNavigationRailVisible() { - final settings = settingsCubit.state; - final viewport = rendererCubit.state.cameraViewport; - return settings.navigationRail && - settings.navigatorPosition == NavigatorPosition.left && - inputCubit.state.hideUi == HideState.visible && - (viewport.width ?? 0) >= LeapBreakpoints.expanded && - (viewport.height ?? 0) >= 400; - } - - Rect? _calculateViewportBounds([ - Area? currentArea, - CameraTransform? customTransform, - ]) { - final settings = settingsCubit.state; - var multiplier = settings.limitViewportMultiplier; - final positive = settings.limitViewportPositive; - - if (multiplier == null && !positive && currentArea == null) return null; - - final viewport = rendererCubit.state.cameraViewport; - final transform = customTransform ?? transformCubit.state; - final navigationRailOffset = _isNavigationRailVisible() - ? kNavigationRailWidth / transform.size - : 0.0; - final size = - Size( - ((viewport.width ?? 0) / transform.size) - navigationRailOffset, - (viewport.height ?? 0) / transform.size, - ) / - settings.renderResolution.multiplier; - - final contentRect = getContentRect(currentArea); - - double minX = double.negativeInfinity; - double minY = double.negativeInfinity; - double maxX = double.infinity; - double maxY = double.infinity; - - if (multiplier != null || currentArea != null) { - multiplier ??= 1; - final padX = size.width * multiplier; - final padY = size.height * multiplier; - - minX = contentRect.left - padX; - minY = contentRect.top - padY; - maxX = contentRect.right - size.width + padX; - maxY = contentRect.bottom - size.height + padY; - - if (minX > maxX) { - final mid = (minX + maxX) / 2; - minX = mid; - maxX = mid; - } - if (minY > maxY) { - final mid = (minY + maxY) / 2; - minY = mid; - maxY = mid; - } - } - - if (positive && currentArea == null) { - minX = max(-navigationRailOffset, minX); - minY = max(0.0, minY); - maxX = max(-navigationRailOffset, maxX); - maxY = max(0.0, maxY); - } - - return Rect.fromLTRB(minX, minY, maxX, maxY); - } - - Area? getRelativeArea(Area currentArea, int dx, int dy, [bool? exact]) { - if (dx == 0 && dy == 0) return null; - final docState = _activeDocumentState; - if (docState is! DocumentLoadSuccess) return null; - - final rect = currentArea.rect.translate( - dx.toDouble() * currentArea.rect.width, - dy.toDouble() * currentArea.rect.height, - ); - - return docState.page.areas.firstWhereOrNull((area) { - final currentAreaRect = area.rect; - if (exact ?? viewCubit.state.areaNavigatorExact) { - return (currentAreaRect.top - rect.top).abs() < - precisionErrorTolerance && - (currentAreaRect.left - rect.left).abs() < - precisionErrorTolerance && - (currentAreaRect.width - rect.width).abs() < - precisionErrorTolerance && - (currentAreaRect.height - rect.height).abs() < - precisionErrorTolerance; - } - return currentAreaRect.overlaps(rect.deflate(precisionErrorTolerance)); - }); - } - - void _teleportToAreaEdge(Area area, int dx, int dy) { - final newBounds = _calculateViewportBounds(area); - if (newBounds == null) return; - - final pos = transformCubit.state.position; - double newX = pos.dx; - double newY = pos.dy; - if (dx > 0) { - newX = newBounds.left; - } else if (dx < 0) { - newX = newBounds.right; - } else { - newX = newX.clamp(newBounds.left, newBounds.right); - } - if (dy > 0) { - newY = newBounds.top; - } else if (dy < 0) { - newY = newBounds.bottom; - } else { - newY = newY.clamp(newBounds.top, newBounds.bottom); - } - transformCubit.teleport(Offset(newX, newY)); - } - - Future navigateToRelativeArea( - int dx, - int dy, { - Future Function()? createAreaName, - }) async { - final docState = _activeDocumentState; - if (docState is! DocumentLoadSuccess) return; - - final current = docState.currentArea; - if (current == null) return; - - var area = getRelativeArea(current, dx, dy); - if (area != null) { - _activeDocumentBloc?.add(CurrentAreaChanged(area.name)); - _teleportToAreaEdge(area, dx, dy); - return; - } - - if (!viewCubit.state.areaNavigatorCreate || createAreaName == null) return; - final name = await createAreaName(); - if (name == null) return; - - final rect = current.rect.translate( - dx.toDouble() * current.rect.width, - dy.toDouble() * current.rect.height, - ); - - final newArea = Area( - position: rect.topLeft.toPoint(), - height: rect.height, - width: rect.width, - name: name, - ); - final bloc = _activeDocumentBloc; - bloc?.add(AreasCreated([AreaPreset(area: newArea)])); - bloc?.add(CurrentAreaChanged(name)); - _teleportToAreaEdge(newArea, dx, dy); - } - - void move(Offset delta, {bool force = false, Area? currentArea}) { - final utilitiesState = viewCubit.state.utilities; - if (!force) { - if (utilitiesState.lockHorizontal) delta = Offset(0, delta.dy); - if (utilitiesState.lockVertical) delta = Offset(delta.dx, 0); - - final bounds = _calculateViewportBounds(currentArea); - if (bounds != null) { - final pos = transformCubit.state.position; - var newPos = pos + delta; - final clampedPos = Offset( - newPos.dx.clamp(bounds.left, bounds.right), - newPos.dy.clamp(bounds.top, bounds.bottom), - ); - - if (currentArea != null && - (clampedPos.dx != newPos.dx || clampedPos.dy != newPos.dy)) { - int dx = 0; - int dy = 0; - if (newPos.dx < bounds.left) { - dx = -1; - } else if (newPos.dx > bounds.right) { - dx = 1; - } - - if (newPos.dy < bounds.top) { - dy = -1; - } else if (newPos.dy > bounds.bottom) { - dy = 1; - } - - if ((dx != 0 || dy != 0) && - settingsCubit.state.hasFlag('edgePanAreaSwitching')) { - final area = getRelativeArea(currentArea, dx, dy); - if (area != null) { - _activeDocumentBloc?.add(CurrentAreaChanged(area.name)); - _teleportToAreaEdge(area, dx, dy); - return; - } - } - } - - delta = clampedPos - pos; - } - } - - if (delta.dx == 0 && delta.dy == 0) { - return; - } - transformCubit.move(delta); - } - - void zoom(double delta, [Offset cursor = Offset.zero, bool force = false]) { - final utilitiesState = viewCubit.state.utilities; - if (utilitiesState.lockZoom && !force) { - delta = 1; - } - if (delta == 1) { - return; - } - if (force) { - transformCubit.zoom(delta, cursor); - return; - } - final transform = transformCubit.state.withSize( - transformCubit.state.size * delta, - cursor, - ); - final clamped = _clampTransform(transform); - transformCubit.teleport(clamped.position, clamped.size); - } - - void size(double size, [Offset cursor = Offset.zero, bool force = false]) { - final utilitiesState = viewCubit.state.utilities; - if (utilitiesState.lockZoom && !force) return; - if (force) { - transformCubit.size(size, cursor); - return; - } - final transform = _clampTransform( - transformCubit.state.withSize(size, cursor), - ); - transformCubit.teleport(transform.position, transform.size); - } - - void slide( - Offset positionVelocity, - double sizeVelocity, { - bool force = false, - Area? currentArea, - }) { - final settings = settingsCubit.state; - if (!settings.hasFlag('smoothNavigation')) return; - final utilitiesState = viewCubit.state.utilities; - Rect? bounds; - var outOfBounds = false; - if (!force) { - if (utilitiesState.lockHorizontal) { - positionVelocity = Offset(0, positionVelocity.dy); - } - if (utilitiesState.lockVertical) { - positionVelocity = Offset(positionVelocity.dx, 0); - } - if (utilitiesState.lockZoom) sizeVelocity = 0; - - bounds = _calculateViewportBounds(currentArea); - if (bounds != null) { - final pos = transformCubit.state.position; - final clampedPos = Offset( - pos.dx.clamp(bounds.left, bounds.right), - pos.dy.clamp(bounds.top, bounds.bottom), - ); - outOfBounds = clampedPos != pos; - var vX = positionVelocity.dx; - var vY = positionVelocity.dy; - - // velocity > 0 means moving right (increasing pos) -> clamp if pos >= maxX - // velocity < 0 means moving left (decreasing pos) -> clamp if pos <= minX - if (pos.dx >= bounds.right && vX > 0) { - vX = 0; - } - if (pos.dx <= bounds.left && vX < 0) { - vX = 0; - } - if (pos.dy >= bounds.bottom && vY > 0) { - vY = 0; - } - if (pos.dy <= bounds.top && vY < 0) { - vY = 0; - } - - positionVelocity = Offset(vX, vY); - } - } - - if (positionVelocity.dx == 0 && - positionVelocity.dy == 0 && - sizeVelocity == 0 && - !outOfBounds) { - return; - } - cancelDelayedBake(); - transformCubit.slide( - positionVelocity, - sizeVelocity, - positionBounds: bounds, - ); - } - - ExternalStorage? getRemoteStorage() => - settingsCubit.getRemote(saveCubit.state.location.remote); - - bool hasAutosave() => - settingsCubit.state.autosave && - (networkingService.isActive || - !(saveCubit.state.embedding?.save ?? true) || - (!kIsWeb && - !saveCubit.state.absolute && - (saveCubit.state.location.isEmpty || - (saveCubit.state.location.fileType?.isNote() ?? false)) && - (saveCubit.state.location.remote.isEmpty || - (settingsCubit - .getRemote(saveCubit.state.location.remote) - ?.hasDocumentCached(saveCubit.state.location.path) ?? - false)))); - - Future save( - DocumentBloc bloc, { - AssetLocation? location, - bool force = false, - bool isAutosave = false, - }) async { - final absolute = saveCubit.state.absolute; - if (location == null && - !force && - (saveCubit.state.saved == SaveState.saved || - saveCubit.state.saved == SaveState.absoluteRead)) { - return saveCubit.state.location; - } - if (networkingService.isClient) { - return AssetLocation.empty; - } - if (saveCubit.state.isSaveDelayed && isAutosave) { - return saveCubit.state.location; - } - final storage = getRemoteStorage(); - final fileSystem = bloc.state.fileSystem.buildDocumentSystem(storage); - final isDelayed = settingsCubit.state.delayedAutosave; - if (isDelayed && isAutosave) { - final seconds = max(0, settingsCubit.state.autosaveDelaySeconds); - saveCubit.setDelayed(true); - await Future.delayed(Duration(seconds: seconds)); - if (!saveCubit.state.isSaveDelayed) { - return saveCubit.state.location; - } - } - return saveCubit.savingLock.synchronized(() async { - if (location == null && - !force && - (saveCubit.state.saved == SaveState.saved || - saveCubit.state.saved == SaveState.absoluteRead)) { - return saveCubit.state.location; - } - var current = location ?? saveCubit.state.location; - if (isClosed) { - return current; - } - saveCubit.setSaveState(saved: SaveState.saving, location: current); - saveCubit.setDelayed(false); - final blocState = bloc.state; - final currentData = await blocState.saveData(); - if (isClosed) { - return current; - } - if (currentData == null || saveCubit.state.embedding != null) { - saveCubit.setSaveState(saved: SaveState.saved); - return AssetLocation.empty; - } - if (absolute || !(current.fileType?.isNote() ?? false)) { - final file = await compute(_toFile, (currentData, false)); - final document = await fileSystem.createFileWithName( - name: currentData.name, - suffix: '.bfly', - directory: absolute - ? null - : current.fileExtension.isEmpty - ? saveCubit.state.location.path - : saveCubit.state.location.parent, - file, - ); - current = document.location; - } else { - final file = await compute(_toFile, ( - currentData, - current.fileType == AssetFileType.textNote, - )); - await fileSystem.updateFile(current.path, file); - } - settingsCubit.addRecentHistory(current); - if (isClosed) { - return current; - } - saveCubit.setSaveState( - saved: saveCubit.state.saved == SaveState.saving - ? SaveState.saved - : saveCubit.state.saved, - location: current, - ); - return current; - }); - } - - Future close() async { - if (_closed) return; - _isClosing = true; - _closed = true; - final bloc = _activeDocumentBloc; - if (bloc != null) { - await toolCubit.disposeRuntime(bloc); - } - _documentBloc = null; - await rendererCubit.disposeRuntime(); - await _transformSubscription?.cancel(); - _transformSubscription = null; - await _rendererSubscription?.cancel(); - _rendererSubscription = null; - await _toolSubscription?.cancel(); - _toolSubscription = null; - await _inputSubscription?.cancel(); - _inputSubscription = null; - await _viewSubscription?.cancel(); - _viewSubscription = null; - _transformDebounceTimer?.cancel(); - _transformDebounceTimer = null; - _networkingDebounceTimer?.cancel(); - _networkingDebounceTimer = null; - await rendererCubit.close(); - await toolCubit.close(); - await inputCubit.close(); - await saveCubit.close(); - await viewCubit.close(); - if (!networkingService.isClosed) { - await networkingService.close(); - } - } - - Rect getPageRect({Set? invisibleLayers}) { - Rect? rect; - for (final renderer in rendererCubit.renderers) { - final rendererRect = renderer.expandedRect; - if (rendererRect == null) continue; - if (invisibleLayers?.contains(renderer.layer) ?? false) { - continue; - } - rect = rect?.expandToInclude(rendererRect) ?? rendererRect; - } - return rect ?? Rect.zero; - } - - /// If addedElements is null, the viewport gets unbaked - Future stateChanged( - DocumentLoadSuccess current, - DocumentBloc bloc, { - DocumentLoadSuccess? oldState, - List> addedElements = const [], - List>? replacedElements, - List>? backgrounds, - bool reset = false, - bool unbake = false, - bool Function()? shouldRefresh, - bool updateIndex = false, - }) async { - cancelDelayedBake(); - for (var renderer in { - ...?backgrounds, - ...?replacedElements, - ...addedElements, - }) { - await renderer.setup( - transformCubit, - current.data, - current.assetService, - current.page, - ); - } - final blocState = bloc.state; - if (blocState is! DocumentLoadSuccess) return; - toolCubit.state.handler.onDocumentUpdated(blocState, oldState); - - final addsCombinedHighlight = addedElements.any( - (renderer) => - renderer is PenRenderer && renderer.element.combineId != null, - ); - if (replacedElements != null) { - await replaceUnbaked(blocState, [ - ...replacedElements, - ...addedElements, - ], backgrounds: backgrounds); - } else if (addsCombinedHighlight) { - await this.unbake( - blocState, - unbakedElements: [...rendererCubit.renderers, ...addedElements], - ); - } else if (unbake) { - await this.unbake(blocState, backgrounds: backgrounds); - } else if (backgrounds != null) { - await this.unbake(blocState, backgrounds: backgrounds); - } else { - await addUnbaked(blocState, addedElements); - } - - setSaveState(saved: SaveState.unsaved); - if (saveCubit.state.embedding != null) { - return; - } - if (reset) { - await reload(bloc, current); - } else { - final refreshRequested = shouldRefresh?.call() == true; - if (refreshRequested) { - // For replacement updates we force a reset bake below, so avoid - // scheduling an extra delayed bake from refresh(). - await refresh(current, allowBake: replacedElements == null); - } - if (replacedElements != null) { - await bake(blocState, reset: true); - } - } - if (updateIndex) { - this.updateIndex(bloc); - } - if (hasAutosave()) { - save(bloc, isAutosave: true); - } - } - - Future updateTogglingTools(DocumentBloc bloc, List tools) async { - final blocState = bloc.state; - if (blocState is! DocumentLoadSuccess) return; - final newHandlers = Map>.from( - toolCubit.state.toggleableHandlers, - ); - final newForegrounds = Map>.from( - toolCubit.state.toggleableForegrounds, - ); - final currentTools = blocState.info.tools; - for (final tool in tools) { - if (tool.id == null) continue; - final index = currentTools.indexWhere((element) => element.id == tool.id); - if (index == -1) continue; - final old = toolCubit.state.toggleableHandlers[index]; - if (old == null) continue; - if (old.data == tool) continue; - old.dispose(bloc); - for (final r in toolCubit.state.toggleableForegrounds[index] ?? []) { - r.dispose(); - } - final handler = Handler.fromTool(tool); - final document = blocState.data; - final page = blocState.page; - final info = blocState.info; - final currentArea = blocState.currentArea; - final foregrounds = handler.createForegrounds( - this, - document, - page, - info, - currentArea, - ); - if (handler.setupForegrounds) { - await Future.wait( - foregrounds.map( - (e) async => await e.setup( - transformCubit, - document, - blocState.assetService, - page, - ), - ), - ); - } - newHandlers[index] = handler; - newForegrounds[index] = foregrounds; - } - toolCubit.setToggleable(handlers: newHandlers, foregrounds: newForegrounds); - } - - void cancelDelayedBake() { - rendererCubit.delayedBakeRunner.cancel(); - } - - Future delayedBake( - DocumentLoaded blocState, { - ui.Size? viewportSize, - double? pixelRatio, - bool reset = false, - bool testTransform = false, - }) => rendererCubit.delayedBakeRunner.schedule(() async { - final newTransform = transformCubit.state; - final viewport = rendererCubit.state.cameraViewport; - - if (testTransform && - newTransform.size == viewport.scale && - newTransform.position == viewport.toOffset()) { - return; - } - - await bake( - blocState, - viewportSize: viewportSize, - pixelRatio: pixelRatio, - reset: reset, - ); - }); - - Future reloadTool( - DocumentBloc bloc, [ - DocumentLoaded? blocState, - ]) async { - final current = blocState ?? bloc.state; - if (current is! DocumentLoaded) return; - final tools = current.info.tools; - final toolIndex = toolCubit.state.index ?? 0; - final newTool = tools.elementAtOrNull(toolIndex); - if (newTool?.isAction() ?? true) { - await changeTool(bloc, index: 0, allowBake: false); - } else if (newTool != toolCubit.state.handler.data) { - await changeTool(bloc, index: toolIndex, allowBake: false); - } - } - - Future reloadRuntime( - DocumentBloc bloc, [ - DocumentLoaded? blocState, - ]) async { - final current = blocState ?? bloc.state; - if (current is! DocumentLoaded) return; - await reloadTool(bloc, current); - await loadElements(current); - await refresh(current, allowBake: false); - await delayedBake(current); - } -} diff --git a/app/lib/cubits/editor_runtime.dart b/app/lib/cubits/editor_runtime.dart index bd5857f5d13c..1dfec60b2f84 100644 --- a/app/lib/cubits/editor_runtime.dart +++ b/app/lib/cubits/editor_runtime.dart @@ -1,3 +1,9 @@ +import 'dart:math'; +import 'dart:ui' as ui; + +import 'package:butterfly/api/image.dart'; +import 'package:butterfly/bloc/document_bloc.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/embed/embedding.dart'; import 'package:butterfly/handlers/handler.dart'; import 'package:butterfly/helpers/async.dart'; @@ -6,17 +12,30 @@ import 'package:butterfly/renderers/renderer.dart'; import 'package:butterfly/selections/selection.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/editor_session.dart'; +import 'package:butterfly/cubits/transform.dart'; +import 'package:butterfly/services/network.dart'; +import 'package:butterfly/services/logger.dart'; +import 'package:butterfly/helpers/xml.dart'; +import 'package:butterfly/view_painter.dart'; import 'package:butterfly/views/navigator/view.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:image/image.dart' as img; import 'package:lw_file_system/lw_file_system.dart'; +import 'package:pdfrx/pdfrx.dart'; import 'package:synchronized/synchronized.dart'; +import 'package:xml/xml.dart'; part 'editor_runtime.freezed.dart'; +Future _toFile((NoteData, bool) args) async { + return args.$1.toFile(isTextBased: args.$2); +} + enum SaveState { saved, saving, unsaved, absoluteRead } enum HideState { visible, keyboard, touch } @@ -55,6 +74,10 @@ class RendererCubit extends Cubit { delay: const Duration(milliseconds: 100), ); + void cancelDelayedBake() { + delayedBakeRunner.cancel(); + } + void replace(RendererRuntimeState state) => emit(state); void setViewport(CameraViewport cameraViewport) => @@ -78,6 +101,932 @@ class RendererCubit extends Cubit { Renderer? getRenderer(PadElement element) => renderers.firstWhereOrNull((renderer) => renderer.element == element); + bool sameRendererList( + List> a, + List> b, + ) { + if (identical(a, b)) return true; + if (a.length != b.length) return false; + for (var i = 0; i < a.length; i++) { + if (!identical(a[i], b[i])) return false; + } + return true; + } + + void invalidateRenderers(Iterable> renderers) { + initializedElements.removeAll(renderers); + } + + Rect getViewportRect(TransformCubit transformCubit, {Size? viewportSize}) { + var size = viewportSize ?? state.cameraViewport.toSize(); + final transform = transformCubit.state; + final resolution = settingsCubit.state.renderResolution; + final friction = transform.friction; + final realWidth = size.width / transform.size; + final realHeight = size.height / transform.size; + Rect rect = Rect.fromLTWH( + transform.position.dx, + transform.position.dy, + realWidth, + realHeight, + ); + if (friction != null) { + final beginPosition = transform.position - friction.beginOffset; + final topLeft = Offset( + min(transform.position.dx, beginPosition.dx), + min(transform.position.dy, beginPosition.dy), + ); + final frictionSize = Size( + realWidth + (friction.beginOffset.dx * transform.size).abs(), + realHeight + (friction.beginOffset.dy * transform.size).abs(), + ); + rect = topLeft & frictionSize; + } + return _snapViewportRect(rect, size, transform, resolution); + } + + Rect _snapViewportRect( + Rect rect, + Size size, + CameraTransform transform, + RenderResolution resolution, + ) { + final screenRect = Rect.fromPoints( + transform.globalToLocal(rect.topLeft), + transform.globalToLocal(rect.bottomRight), + ); + final snappedRect = _expandScreenRect( + Rect.fromLTRB( + screenRect.left.floorToDouble(), + screenRect.top.floorToDouble(), + screenRect.right.ceilToDouble(), + screenRect.bottom.ceilToDouble(), + ), + Size( + (size.width * resolution.multiplier).ceilToDouble(), + (size.height * resolution.multiplier).ceilToDouble(), + ), + ); + return Rect.fromPoints( + transform.localToGlobal(snappedRect.topLeft), + transform.localToGlobal(snappedRect.bottomRight), + ); + } + + Rect _expandScreenRect(Rect rect, Size minimumSize) { + final dx = max(0.0, minimumSize.width - rect.width); + final dy = max(0.0, minimumSize.height - rect.height); + return Rect.fromLTRB( + rect.left - (dx / 2).floorToDouble(), + rect.top - (dy / 2).floorToDouble(), + rect.right + (dx / 2).ceilToDouble(), + rect.bottom + (dy / 2).ceilToDouble(), + ); + } + + bool rectContains(Rect outer, Rect inner) { + const tolerance = precisionErrorTolerance; + return outer.left <= inner.left + tolerance && + outer.top <= inner.top + tolerance && + outer.right >= inner.right - tolerance && + outer.bottom >= inner.bottom - tolerance; + } + + Rect getPageRect({Set? invisibleLayers}) { + Rect? rect; + for (final renderer in renderers) { + final rendererRect = renderer.expandedRect; + if (rendererRect == null) continue; + if (invisibleLayers?.contains(renderer.layer) ?? false) continue; + rect = rect?.expandToInclude(rendererRect) ?? rendererRect; + } + return rect ?? Rect.zero; + } + + Future updateVisibleElements( + EditorController controller, + DocumentBloc? bloc, + ) async { + if (controller.isClosed) return; + final unbaked = state.cameraViewport.unbakedElements; + final baked = state.cameraViewport.bakedElements; + + final rect = getViewportRect(controller.transformCubit); + final currentVisible = state.cameraViewport.visibleElements; + final currentVisibleUnbaked = state.cameraViewport.visibleUnbakedElements; + + final visibleUnbaked = unbaked.where((e) => e.isVisible(rect)).toList(); + final visible = >[ + ...baked.where((e) => e.isVisible(rect)), + ...visibleUnbaked, + ]; + + if (sameRendererList(visible, currentVisible) && + sameRendererList(visibleUnbaked, currentVisibleUnbaked)) { + return; + } + + final newViewport = state.cameraViewport.withUnbaked( + unbaked, + visibleElements: visible, + visibleUnbakedElements: visibleUnbaked, + ); + + final docState = bloc?.state; + if (docState is DocumentLoaded) { + await updateOnVisible(controller, newViewport, docState); + if (!controller.isClosed && bloc != null && !bloc.isClosed) { + bloc.delayedBake(); + } + } + + if (controller.isClosed) return; + + setViewport(newViewport); + } + + Future updateOnVisible( + EditorController controller, + CameraViewport newViewport, + DocumentLoaded blocState, { + CameraTransform? renderTransform, + ui.Size? targetSize, + }) async { + final newVisibleList = newViewport.visibleElements; + final nextVisibleSet = newVisibleList.toSet(); + + final newVisible = newVisibleList + .where((e) => !initializedElements.contains(e)) + .toList(); + + final newlyHidden = initializedElements + .where((e) => !nextVisibleSet.contains(e)) + .toList(); + + if (newVisible.isEmpty && newlyHidden.isEmpty) return; + + final transform = renderTransform ?? controller.transformCubit.state; + final size = targetSize ?? newViewport.toSize(); + + initializedElements.removeAll(newlyHidden); + + if (newVisible.isNotEmpty) { + talker.verbose('Updating visible elements: ${newVisible.length} new'); + final initialized = await Future.wait( + newVisible.map((element) async { + try { + await Future.sync( + () => element.onVisible(controller, blocState, transform, size), + ); + return element; + } catch (error, stackTrace) { + talker.error( + 'Failed to initialize visible renderer $element', + error, + stackTrace, + ); + } + return null; + }), + ); + initializedElements.addAll(initialized.nonNulls); + } + + if (newlyHidden.isNotEmpty) { + await Future.wait( + newlyHidden.map( + (element) async => + await element.onHidden(controller, blocState, transform, size), + ), + ); + } + } + + Future delayedBake( + EditorController controller, + DocumentLoaded blocState, { + ui.Size? viewportSize, + double? pixelRatio, + bool reset = false, + bool testTransform = false, + }) => delayedBakeRunner.schedule(() async { + final newTransform = controller.transformCubit.state; + final viewport = state.cameraViewport; + + if (testTransform && + newTransform.size == viewport.scale && + newTransform.position == viewport.toOffset()) { + return; + } + + await controller.rendererCubit.bake( + controller, + blocState, + viewportSize: viewportSize, + pixelRatio: pixelRatio, + reset: reset, + ); + }); + + Future bake( + EditorController controller, + DocumentLoaded blocState, { + Size? viewportSize, + double? pixelRatio, + bool reset = false, + bool resetAllLayers = false, + }) => bakeLock.synchronized(() async { + final rendererCubit = this; + final transformCubit = controller.transformCubit; + final settingsCubit = controller.settingsCubit; + if (controller.isClosed) return; + var cameraViewport = rendererCubit.state.cameraViewport; + final startTransform = transformCubit.state; + final startViewport = cameraViewport; + final resolution = settingsCubit.state.renderResolution; + var size = viewportSize ?? cameraViewport.toSize(); + final ratio = pixelRatio ?? cameraViewport.pixelRatio; + if (size.height <= 0 || size.width <= 0) { + return; + } + if (viewportSize == null) { + size /= resolution.multiplier; + } + var transform = transformCubit.state; + var renderers = List>.from(rendererCubit.renderers); + final recorder = ui.PictureRecorder(); + final canvas = ui.Canvas(recorder); + final rect = rendererCubit.getViewportRect( + transformCubit, + viewportSize: size, + ); + size = rect.size * transform.size; + final renderTransform = transform.improve(resolution, rect); + final document = blocState.data; + final page = blocState.page; + final info = blocState.info; + final imageWidth = (size.width * ratio).ceil(); + final imageHeight = (size.height * ratio).ceil(); + var allRendererStates = rendererCubit.state.allRendererStates; + final rendererStatesChanged = !mapEquals( + allRendererStates, + cameraViewport.rendererStates, + ); + if (!rendererStatesChanged) { + allRendererStates = cameraViewport.rendererStates; + } + final invisibleLayers = blocState.invisibleLayers; + final viewportAlreadyCoversRect = + cameraViewport.image != null && + cameraViewport.scale == transform.size && + cameraViewport.resolution == resolution && + cameraViewport.pixelRatio == ratio && + !rendererStatesChanged && + setEquals(cameraViewport.invisibleLayers, invisibleLayers) && + rendererCubit.rectContains(cameraViewport.toRect(), rect); + final viewChanged = + !viewportAlreadyCoversRect && + (cameraViewport.width != size.width.ceil() || + cameraViewport.height != size.height.ceil() || + cameraViewport.pixelRatio != ratio || + cameraViewport.resolution != resolution || + cameraViewport.x != renderTransform.position.dx || + cameraViewport.y != renderTransform.position.dy || + cameraViewport.scale != transform.size || + rendererStatesChanged || + !setEquals(cameraViewport.invisibleLayers, invisibleLayers)); + reset = reset || viewChanged; + resetAllLayers = resetAllLayers || viewChanged; + if (cameraViewport.unbakedElements.isEmpty && !reset) return; + final currentLayer = blocState.currentLayer; + List> visibleElements; + final oldVisible = cameraViewport.visibleElements; + final oldVisibleSet = oldVisible.toSet(); + talker.verbose( + 'Baking viewport (reset: $reset, viewChanged: $viewChanged, ' + 'rendererStatesChanged: $rendererStatesChanged)', + ); + + if (reset) { + visibleElements = renderers + .where((renderer) => renderer.isVisible(rect)) + .toList(); + } else { + visibleElements = List.from(oldVisible) + ..addAll( + cameraViewport.unbakedElements.where( + (renderer) => + !oldVisibleSet.contains(renderer) && renderer.isVisible(rect), + ), + ); + } + + final visibleElementsSet = visibleElements.toSet(); + + await rendererCubit.updateOnVisible( + controller, + cameraViewport.unbake(visibleElements: visibleElements), + blocState, + renderTransform: renderTransform, + targetSize: size, + ); + + canvas.scale(ratio); + + if (viewChanged && visibleElements.isNotEmpty) { + await Future.wait( + visibleElements.map( + (e) async => + await e.updateView(controller, blocState, renderTransform, size), + ), + ); + } + + // Wait one frame + await Future.delayed(const Duration(milliseconds: 1)); + + ViewPainter( + document, + page, + info, + transform: renderTransform, + cameraViewport: reset + ? cameraViewport.unbake( + rendererStates: allRendererStates, + unbakedElements: visibleElements + .where((e) => currentLayer == e.layer) + .toList(), + visibleElements: visibleElements, + ) + : cameraViewport, + renderBackground: false, + renderBaked: !reset, + renderBakedLayers: false, + invisibleLayers: invisibleLayers, + ).paint(canvas, size); + + final picture = recorder.endRecording(); + ui.Image newImage; + try { + newImage = await picture.toImage(imageWidth, imageHeight); + } finally { + picture.dispose(); + } + + var belowLayerImage = cameraViewport.belowLayerImage; + var aboveLayerImage = cameraViewport.aboveLayerImage; + + if (resetAllLayers) { + final belowLayerRecorder = ui.PictureRecorder(); + final belowLayerCanvas = ui.Canvas(belowLayerRecorder); + belowLayerCanvas.scale(ratio); + final aboveLayerRecorder = ui.PictureRecorder(); + final aboveLayerCanvas = ui.Canvas(aboveLayerRecorder); + aboveLayerCanvas.scale(ratio); + final belowLayers = {}, aboveLayers = {}; + bool above = false; + for (final layer in page.layers) { + if (layer.id == currentLayer) { + above = true; + continue; + } + final layerId = layer.id; + if (layerId == null) continue; + if (above) { + aboveLayers.add(layerId); + } else { + belowLayers.add(layerId); + } + } + + ViewPainter( + document, + page, + info, + transform: renderTransform, + cameraViewport: cameraViewport.unbake( + rendererStates: allRendererStates, + unbakedElements: visibleElements + .where((e) => e.layer != null && belowLayers.contains(e.layer)) + .toList(), + visibleElements: visibleElements, + ), + renderBackground: false, + renderBaked: false, + invisibleLayers: invisibleLayers, + ).paint(belowLayerCanvas, size); + ViewPainter( + document, + page, + info, + transform: renderTransform, + cameraViewport: cameraViewport.unbake( + rendererStates: allRendererStates, + unbakedElements: visibleElements + .where((e) => e.layer != null && aboveLayers.contains(e.layer)) + .toList(), + visibleElements: visibleElements, + ), + renderBackground: false, + renderBaked: false, + invisibleLayers: invisibleLayers, + ).paint(aboveLayerCanvas, size); + + final belowPicture = belowLayerRecorder.endRecording(); + final abovePicture = aboveLayerRecorder.endRecording(); + try { + final result = await Future.wait([ + belowPicture.toImage(imageWidth, imageHeight), + abovePicture.toImage(imageWidth, imageHeight), + ]); + belowLayerImage = result[0]; + aboveLayerImage = result[1]; + } finally { + belowPicture.dispose(); + abovePicture.dispose(); + } + } + + final bakedElementsSet = cameraViewport.bakedElements + .map((e) => e.element) + .toSet(); + final unbakedElementsSet = cameraViewport.unbakedElements + .map((e) => e.element) + .toSet(); + + final newlyUnbaked = + (reset + ? rendererCubit.renderers + : rendererCubit.state.cameraViewport.unbakedElements) + .where( + (element) => + !bakedElementsSet.contains(element.element) && + !unbakedElementsSet.contains(element.element) && + !visibleElementsSet.contains(element), + ) + .toList(); + + if (controller.isClosed) return; + + // If state changed while baking (e.g. fast move submitted a newer viewport), + // this bake output is stale and must not overwrite the latest viewport. + final currentViewport = rendererCubit.state.cameraViewport; + final currentTransform = transformCubit.state; + if (!identical(currentViewport, startViewport) || + currentTransform != startTransform) { + newImage.dispose(); + final oldBelow = startViewport.belowLayerImage; + final oldAbove = startViewport.aboveLayerImage; + if (!identical(belowLayerImage, oldBelow)) { + belowLayerImage?.dispose(); + } + if (!identical(aboveLayerImage, oldAbove)) { + aboveLayerImage?.dispose(); + } + Future.microtask(() async { + final latestState = controller.activeDocumentState; + if (latestState == null) return; + await bake( + controller, + latestState, + viewportSize: viewportSize, + pixelRatio: pixelRatio, + reset: reset, + resetAllLayers: resetAllLayers, + ); + }); + return; + } + + final newViewport = cameraViewport.bake( + height: size.height, + width: size.width, + pixelRatio: ratio, + resolution: resolution, + scale: transform.size, + x: renderTransform.position.dx, + y: renderTransform.position.dy, + image: newImage, + bakedElements: renderers, + unbakedElements: newlyUnbaked, + visibleElements: visibleElements, + visibleUnbakedElements: newlyUnbaked + .where((renderer) => renderer.isVisible(rect)) + .toList(), + belowLayerImage: belowLayerImage, + aboveLayerImage: aboveLayerImage, + rendererStates: allRendererStates, + invisibleLayers: invisibleLayers, + ); + rendererCubit.setViewport(newViewport); + }); + + Future renderImage( + EditorController controller, + NoteData document, + DocumentPage page, + DocumentInfo info, + ImageExportOptions options, { + CameraViewport? cameraViewport, + Set? invisibleLayers, + DocumentLoaded? docState, + }) async { + final rendererCubit = this; + final realWidth = (options.width * options.quality).ceil(); + final realHeight = (options.height * options.quality).ceil(); + final realZoom = options.scale; + if (realWidth <= 0 || realHeight <= 0) { + return null; + } + final size = Size(options.width, options.height); + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + canvas.scale(options.quality); + final viewport = + cameraViewport ?? + rendererCubit.state.cameraViewport.unbake( + unbakedElements: rendererCubit.renderers, + ); + final transform = CameraTransform( + options.quality, + Offset(options.x, options.y), + realZoom, + ); + final hiddenRenderers = >[]; + if (docState != null) { + final exportRect = Rect.fromLTWH( + options.x, + options.y, + options.width, + options.height, + ); + for (final renderer in viewport.unbakedElements) { + if (renderer.isVisible(exportRect)) { + final wasInitialized = rendererCubit.initializedElements.contains( + renderer, + ); + if (!wasInitialized) { + await renderer.onVisible(controller, docState, transform, size); + hiddenRenderers.add(renderer); + } + } + } + } + final painter = ViewPainter( + document, + page, + info, + renderBackground: options.renderBackground, + invisibleLayers: invisibleLayers, + cameraViewport: viewport, + transform: transform, + ); + painter.paint(canvas, size); + for (final renderer in hiddenRenderers) { + await renderer.onHidden(controller, docState!, transform, size); + } + final picture = recorder.endRecording(); + ui.Image? image; + try { + image = await picture.toImage(realWidth, realHeight); + } finally { + picture.dispose(); + } + return image; + } + + Future render( + EditorController controller, + NoteData document, + DocumentPage page, + DocumentInfo info, + ImageExportOptions options, { + CameraViewport? cameraViewport, + Set? invisibleLayers, + DocumentLoaded? docState, + }) async { + final image = await renderImage( + controller, + document, + page, + info, + options, + cameraViewport: cameraViewport, + invisibleLayers: invisibleLayers, + docState: docState, + ); + ByteData? bytes; + try { + bytes = await image?.toByteData(format: ui.ImageByteFormat.png); + } finally { + image?.dispose(); + } + return bytes; + } + + XmlDocument renderSVG( + NoteData document, + DocumentPage page, + SvgExportOptions options, { + Set? invisibleLayers, + }) { + final rendererCubit = this; + final xml = XmlDocument(); + xml.createElement( + 'svg', + attributes: { + 'xmlns': 'http://www.w3.org/2000/svg', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + 'version': '1.1', + 'width': '${options.width}px', + 'height': '${options.height}px', + 'viewBox': + '${options.x} ${options.y} ${options.width} ${options.height}', + }, + ); + + final rect = Rect.fromLTWH( + options.x, + options.y, + options.width.toDouble(), + options.height.toDouble(), + ); + if (options.renderBackground) { + for (final e in rendererCubit.state.cameraViewport.backgrounds) { + e.buildSvg(xml, document, page, rect); + } + } + for (var e in rendererCubit.renderers) { + if ((invisibleLayers?.contains(e.layer) ?? false) || !e.isVisible(rect)) { + continue; + } + e.buildSvg(xml, document, page, rect); + } + return xml; + } + + Future unbake( + EditorController controller, + DocumentLoaded blocState, { + List>? backgrounds, + List>? unbakedElements, + }) async { + final rendererCubit = this; + final transformCubit = controller.transformCubit; + final elementsToCheck = unbakedElements ?? rendererCubit.renderers; + final oldViewport = rendererCubit.state.cameraViewport; + final newViewport = oldViewport.unbake( + unbakedElements: unbakedElements, + visibleElements: elementsToCheck + .where( + (e) => e.isVisible(rendererCubit.getViewportRect(transformCubit)), + ) + .toList(), + backgrounds: backgrounds, + ); + await rendererCubit.updateOnVisible(controller, newViewport, blocState); + rendererCubit.setViewport(newViewport); + } + + Future replaceUnbaked( + EditorController controller, + DocumentLoaded blocState, + List> unbakedElements, { + List>? backgrounds, + }) async { + final rendererCubit = this; + final transformCubit = controller.transformCubit; + final visibleElements = unbakedElements + .where( + (e) => e.isVisible(rendererCubit.getViewportRect(transformCubit)), + ) + .toList(); + final newViewport = rendererCubit.state.cameraViewport.replaceUnbaked( + unbakedElements, + visibleElements: visibleElements, + visibleUnbakedElements: visibleElements, + backgrounds: backgrounds, + ); + await rendererCubit.updateOnVisible(controller, newViewport, blocState); + rendererCubit.setViewport(newViewport); + } + + Future loadElements( + EditorController controller, + DocumentState docState, { + bool reset = false, + }) async { + final rendererCubit = this; + final transformCubit = controller.transformCubit; + if (docState is! DocumentLoaded) return; + final document = docState.data; + final assetService = docState.assetService; + final page = docState.page; + var existing = rendererCubit.renderers; + if (reset) { + for (var e in existing) { + rendererCubit.initializedElements.remove(e); + e.dispose(); + } + existing = []; + } + final elements = page.layers + .where((e) => !docState.invisibleLayers.contains(e.id)) + .expand((l) => l.content.map((e) => (e, l.id))) + .toList(); + final elementKeys = elements + .map((element) => (element.$1, element.$2)) + .toSet(); + final existingByKey = { + for (final renderer in existing) + (renderer.element, renderer.layer): renderer, + }; + final reusable = >[]; + final reusableKeys = <(PadElement, String?)>{}; + for (final element in elements) { + final key = (element.$1, element.$2); + final renderer = existingByKey[key]; + if (renderer != null) { + reusable.add(renderer); + reusableKeys.add(key); + } + } + final dropped = existing + .where( + (renderer) => + !elementKeys.contains((renderer.element, renderer.layer)), + ) + .toList(); + for (final e in dropped) { + rendererCubit.initializedElements.remove(e); + e.dispose(); + } + final newRenderers = elements + .where((e) => !reusableKeys.contains((e.$1, e.$2))) + .map((e) => Renderer.fromInstance(e.$1, e.$2)) + .toList(); + await Future.wait( + newRenderers.map( + (e) async => + await e.setup(transformCubit, document, assetService, page), + ), + ); + // Build layer index map for O(1) lookups instead of O(n) indexOf calls + final layersList = page.layers.map((e) => e.id).toList(); + final layerIndexMap = {}; + for (var i = 0; i < layersList.length; i++) { + layerIndexMap[layersList[i]] = i; + } + + // Build element index map for O(1) lookups + final elementIndexMap = {}; + for (var i = 0; i < elements.length; i++) { + elementIndexMap[elements[i].$1] = i; + } + + final combined = [...reusable, ...newRenderers] + ..sort((a, b) { + final layerA = layerIndexMap[a.layer] ?? layersList.length; + final layerB = layerIndexMap[b.layer] ?? layersList.length; + if (layerA != layerB) return layerA.compareTo(layerB); + final indexA = elementIndexMap[a.element] ?? -1; + final indexB = elementIndexMap[b.element] ?? -1; + return indexA.compareTo(indexB); + }); + final backgrounds = page.backgrounds.map(Renderer.fromInstance).toList(); + await Future.wait( + backgrounds.map( + (e) async => + await e.setup(transformCubit, document, assetService, page), + ), + ); + final rect = rendererCubit.getViewportRect(transformCubit); + final visibleElements = combined.where((e) => e.isVisible(rect)).toList(); + final oldViewport = rendererCubit.state.cameraViewport; + final newViewport = oldViewport.unbake( + unbakedElements: combined, + visibleElements: visibleElements, + backgrounds: backgrounds, + ); + await rendererCubit.updateOnVisible(controller, newViewport, docState); + controller.saveCubit.setSaveState( + location: + controller.saveCubit.state.embedding?.location ?? + controller.saveCubit.state.location, + ); + rendererCubit.setViewport(newViewport); + } + + Future addUnbaked( + EditorController controller, + DocumentLoaded blocState, + List> unbakedElements, [ + List>? visibleElements, + ]) async { + final rendererCubit = this; + final transformCubit = controller.transformCubit; + final rect = rendererCubit.getViewportRect(transformCubit); + visibleElements ??= unbakedElements + .where((e) => e.isVisible(rect)) + .toList(); + final nextUnbaked = [ + ...rendererCubit.state.cameraViewport.unbakedElements, + ...unbakedElements, + ]; + final newViewport = rendererCubit.state.cameraViewport.withUnbaked( + nextUnbaked, + visibleElements: [ + ...rendererCubit.state.cameraViewport.visibleElements, + ...visibleElements, + ], + visibleUnbakedElements: [ + ...rendererCubit.state.cameraViewport.visibleUnbakedElements, + ...visibleElements, + ], + ); + await rendererCubit.updateOnVisible(controller, newViewport, blocState); + rendererCubit.setViewport(newViewport); + } + + Future renderPDF( + EditorController controller, + DocumentLoaded docState, { + required List areas, + bool renderBackground = true, + void Function(double progress)? onProgress, + Set? invisibleLayers, + }) async { + final transformCubit = controller.transformCubit; + var name = docState.metadata.name; + if (name.isEmpty) { + name = 'document'; + } + final pdf = await PdfDocument.createNew(sourceName: '$name.pdf'); + final document = docState.data; + final info = docState.info; + final pages = []; + final documents = []; + for (var i = 0; i < areas.length; i++) { + onProgress?.call(i / areas.length); + final preset = areas[i]; + final areaName = preset.name; + final quality = preset.quality; + final currentOpened = docState.pageName == preset.page; + final page = currentOpened + ? docState.page + : document.getPage(preset.page); + final area = preset.area ?? page?.getAreaByName(areaName); + if (area == null || page == null) { + continue; + } + final image = await renderImage( + controller, + document, + page, + info, + ImageExportOptions( + width: area.width, + height: area.height, + x: area.position.x, + y: area.position.y, + quality: quality, + renderBackground: renderBackground, + ), + cameraViewport: await CameraViewport.build( + transformCubit, + document, + docState.assetService, + page, + ), + docState: docState, + invisibleLayers: invisibleLayers ?? docState.invisibleLayers, + ); + if (image == null) continue; + final imgImage = await convertFlutterUiToImage(image); + final pdfImage = await compute( + (image) => img.JpegEncoder().encode(image), + imgImage, + ); + final imageDoc = await PdfDocument.createFromJpegData( + pdfImage, + width: area.width, + height: area.height, + sourceName: '$name-$areaName.jpg', + ); + pages.addAll(imageDoc.pages); + image.dispose(); + documents.add(imageDoc); + } + onProgress?.call(1.0); + pdf.pages = pages; + final bytes = await pdf.encodePdf(); + pdf.dispose(); + for (final doc in documents) { + doc.dispose(); + } + return bytes; + } + Future disposeRuntime() async { delayedBakeRunner.cancel(); await delayedBakeRunner.disposeAndWait(); @@ -262,7 +1211,8 @@ class ToolCubit extends Cubit { Selection? selection; if (selected is Selection?) { selection = selected; - } else if (!toggle || !(state.selection?.selected.contains(selected) ?? false)) { + } else if (!toggle || + !(state.selection?.selected.contains(selected) ?? false)) { selection = Selection.from(selected); } setSelection(selection); @@ -283,7 +1233,10 @@ class ToolCubit extends Cubit { Tool? getTool(DocumentInfo info) { final index = state.index; - if (index == null || info.tools.isEmpty || index < 0 || index >= info.tools.length) { + if (index == null || + info.tools.isEmpty || + index < 0 || + index >= info.tools.length) { return null; } return info.tools[index]; @@ -319,6 +1272,757 @@ class ToolCubit extends Cubit { rendererCubit.setRendererStates(temporaryRendererStates: const {}); } + Future updateHandler( + DocumentBloc bloc, + RendererCubit rendererCubit, + Handler handler, + ) async { + replace( + state.copyWith( + handler: handler, + cursor: handler.cursor ?? MouseCursor.defer, + toolbar: await handler.getToolbar(bloc), + ), + ); + rendererCubit.setRendererStates(rendererStates: handler.rendererStates); + } + + Future updateTool( + EditorController controller, + DocumentBloc bloc, + Tool tool, + ) async { + final docState = bloc.state; + if (docState is! DocumentLoadSuccess) return; + state.handler.dispose(bloc); + final handler = Handler.fromTool(tool); + for (final renderer in state.foregrounds) { + renderer.dispose(); + } + final foregrounds = handler.createForegrounds( + controller, + docState.data, + docState.page, + docState.info, + docState.currentArea, + ); + if (handler.setupForegrounds) { + await Future.wait( + foregrounds.map( + (e) async => await e.setup( + controller.transformCubit, + docState.data, + docState.assetService, + docState.page, + ), + ), + ); + } + setActiveTool( + index: state.index, + handler: handler, + cursor: handler.cursor ?? MouseCursor.defer, + foregrounds: foregrounds, + toolbar: await handler.getToolbar(bloc), + rendererStates: handler.rendererStates, + ); + controller.rendererCubit.setRendererStates( + rendererStates: handler.rendererStates, + ); + } + + Future updateTemporaryTool( + EditorController controller, + DocumentBloc bloc, + Tool tool, + ) async { + final docState = bloc.state; + if (docState is! DocumentLoadSuccess) return; + state.temporaryHandler?.dispose(bloc); + final handler = Handler.fromTool(tool); + for (final renderer in state.temporaryForegrounds ?? const []) { + renderer.dispose(); + } + final foregrounds = handler.createForegrounds( + controller, + docState.data, + docState.page, + docState.info, + docState.currentArea, + ); + if (handler.setupForegrounds) { + await Future.wait( + foregrounds.map( + (e) async => await e.setup( + controller.transformCubit, + docState.data, + docState.assetService, + docState.page, + ), + ), + ); + } + setTemporaryTool( + handler: handler, + index: state.temporaryIndex, + foregrounds: foregrounds, + toolbar: await handler.getToolbar(bloc), + cursor: handler.cursor, + rendererStates: handler.rendererStates, + ); + controller.rendererCubit.setRendererStates( + temporaryRendererStates: handler.rendererStates, + ); + } + + Future updateTogglingTools( + EditorController controller, + DocumentBloc bloc, + List tools, + ) async { + final blocState = bloc.state; + if (blocState is! DocumentLoadSuccess) return; + final newHandlers = Map>.from(state.toggleableHandlers); + final newForegrounds = Map>.from( + state.toggleableForegrounds, + ); + final currentTools = blocState.info.tools; + for (final tool in tools) { + if (tool.id == null) continue; + final index = currentTools.indexWhere((element) => element.id == tool.id); + if (index == -1) continue; + final old = state.toggleableHandlers[index]; + if (old == null || old.data == tool) continue; + old.dispose(bloc); + for (final renderer in state.toggleableForegrounds[index] ?? []) { + renderer.dispose(); + } + final handler = Handler.fromTool(tool); + final foregrounds = handler.createForegrounds( + controller, + blocState.data, + blocState.page, + blocState.info, + blocState.currentArea, + ); + if (handler.setupForegrounds) { + await Future.wait( + foregrounds.map( + (e) async => await e.setup( + controller.transformCubit, + blocState.data, + blocState.assetService, + blocState.page, + ), + ), + ); + } + newHandlers[index] = handler; + newForegrounds[index] = foregrounds; + } + setToggleable(handlers: newHandlers, foregrounds: newForegrounds); + } + + void disposeForegrounds() { + for (final r in state.foregrounds) { + r.dispose(); + } + } + + void disposeTemporaryForegrounds() { + for (final r in state.temporaryForegrounds ?? []) { + r.dispose(); + } + } + + void disposeNetworkingForegrounds() { + for (final r in state.networkingForegrounds) { + r.dispose(); + } + } + + void disposeToggleableForegrounds() { + for (final r in state.toggleableForegrounds.values.expand((e) => e)) { + r.dispose(); + } + } + + void disposeAllForegrounds() { + disposeForegrounds(); + disposeTemporaryForegrounds(); + disposeNetworkingForegrounds(); + disposeToggleableForegrounds(); + } + + R useHandler( + DocumentBloc bloc, + int index, + R Function(Handler handler) callback, { + required bool editable, + }) { + Handler? handler; + bool needsDispose = false; + if (state.index == index) { + handler = fetchHandler>( + disableTemporary: true, + editable: editable, + ); + } else if (state.toggleableHandlers.containsKey(index)) { + handler = state.toggleableHandlers[index]; + } + if (handler == null) { + List tools = const []; + final blocState = bloc.state; + if (blocState is DocumentLoaded) tools = blocState.info.tools; + final tool = tools.elementAtOrNull(index) ?? HandTool(); + handler = Handler.fromTool(tool); + needsDispose = true; + } + final result = callback(handler); + if (needsDispose) { + if (result is Future) { + result.then((value) => handler?.dispose(bloc)); + } else { + handler.dispose(bloc); + } + } + return result; + } + + Future changeTool( + EditorController controller, + DocumentBloc bloc, { + int? index, + BuildContext? context, + Handler? handler, + bool allowBake = true, + }) async { + talker.verbose('Changing tool to index: $index'); + await resetInput(bloc, controller.inputCubit); + final blocState = bloc.state; + if (blocState is! DocumentLoadSuccess) return null; + if (controller.saveCubit.state.embedding?.editable == false) { + return null; + } + final document = blocState.data; + final info = blocState.info; + index ??= state.index ?? 0; + if (handler == null && (index < 0 || index >= info.tools.length)) { + return null; + } + handler ??= Handler.fromTool(info.tools[index]); + var selectState = SelectState.normal; + if (context != null) { + selectState = await handler.onSelected(context); + } + if (selectState != SelectState.none) { + state.handler.dispose(bloc); + state.temporaryHandler?.dispose(bloc); + disposeTemporaryForegrounds(); + disposeForegrounds(); + final foregrounds = handler.createForegrounds( + controller, + document, + blocState.page, + info, + blocState.currentArea, + ); + if (handler.setupForegrounds) { + await Future.wait( + foregrounds.map( + (e) async => await e.setup( + controller.transformCubit, + document, + blocState.assetService, + blocState.page, + ), + ), + ); + } + if (selectState == SelectState.normal) { + controller.editorSessionCubit?.updateSelectedTool(handler.data, index); + setActiveTool( + index: index, + handler: handler, + cursor: handler.cursor ?? MouseCursor.defer, + foregrounds: foregrounds, + toolbar: await handler.getToolbar(bloc), + rendererStates: handler.rendererStates, + ); + controller.rendererCubit.setRendererStates( + rendererStates: handler.rendererStates, + temporaryRendererStates: const {}, + ); + if (allowBake) { + await controller.rendererCubit.bake(controller, blocState); + } + } else { + if (isHandlerEnabled(index)) { + disableHandler(bloc, index); + } else { + setToggleable( + handlers: {...state.toggleableHandlers, index: handler}, + foregrounds: {...state.toggleableForegrounds, index: foregrounds}, + ); + } + } + } + return handler; + } + + Future toggleHandler( + EditorController controller, + DocumentBloc bloc, + int index, + ) async { + if (state.toggleableHandlers.containsKey(index)) { + disableHandler(bloc, index); + } else { + await enableHandler(controller, bloc, index); + } + } + + Future enableHandler( + EditorController controller, + DocumentBloc bloc, + int index, + ) async { + final blocState = bloc.state; + if (blocState is! DocumentLoaded) return null; + if (index < 0 || index >= blocState.info.tools.length) { + return null; + } + final tool = blocState.info.tools[index]; + final handler = Handler.fromTool(tool); + final document = blocState.data; + final page = blocState.page; + final info = blocState.info; + final currentArea = blocState.currentArea; + final foregrounds = handler.createForegrounds( + controller, + document, + page, + info, + currentArea, + ); + if (handler.setupForegrounds) { + await Future.wait( + foregrounds.map( + (e) async => await e.setup( + controller.transformCubit, + document, + blocState.assetService, + page, + ), + ), + ); + } + setToggleable( + handlers: Map.from(state.toggleableHandlers)..[index] = handler, + foregrounds: Map.from(state.toggleableForegrounds)..[index] = foregrounds, + ); + return handler; + } + + bool disableHandler(DocumentBloc bloc, int index) { + final handler = state.toggleableHandlers[index]; + if (handler == null) { + return false; + } + handler.dispose(bloc); + final foregrounds = Map>.from( + state.toggleableForegrounds, + ); + final current = foregrounds.remove(index); + for (final r in current ?? []) { + r.dispose(); + } + setToggleable( + handlers: Map.from(state.toggleableHandlers)..remove(index), + foregrounds: foregrounds, + ); + return true; + } + + bool isHandlerEnabled(int index) => + state.toggleableHandlers.containsKey(index); + + void reset(EditorController controller, DocumentBloc bloc) { + for (final r in controller.rendererCubit.renderers) { + r.dispose(); + } + controller.rendererCubit.initializedElements.clear(); + state.handler.dispose(bloc); + state.temporaryHandler?.dispose(bloc); + for (var e in state.toggleableHandlers.values) { + e.dispose(bloc); + } + disposeAllForegrounds(); + resetRuntime(); + controller.rendererCubit.replace(const RendererRuntimeState()); + } + + Future changeTemporaryHandlerIndex( + BuildContext context, + EditorController controller, + int index, { + DocumentBloc? bloc, + TemporaryState temporaryState = TemporaryState.allowClick, + bool force = false, + }) async { + bloc ??= context.read(); + final blocState = bloc.state; + if (blocState is! DocumentLoadSuccess) return null; + if (index < 0 || index >= blocState.info.tools.length) { + return null; + } + final tool = blocState.info.tools[index]; + final temporaryHandler = state.temporaryHandler; + if (!force && index == state.temporaryIndex && temporaryHandler != null) { + return temporaryHandler; + } + return changeTemporaryHandler( + context, + controller, + tool, + bloc: bloc, + temporaryState: temporaryState, + index: index, + ); + } + + Future?> changeTemporaryHandler( + BuildContext context, + EditorController controller, + T tool, { + DocumentBloc? bloc, + int? index, + TemporaryState temporaryState = TemporaryState.allowClick, + }) async { + bloc ??= context.read(); + final handler = Handler.fromTool(tool); + final blocState = bloc.state; + if (blocState is! DocumentLoadSuccess) return null; + final document = blocState.data; + final page = blocState.page; + final currentArea = blocState.currentArea; + state.temporaryHandler?.dispose(bloc); + final selectState = await handler.onSelected(context); + + if (selectState == SelectState.normal) { + disposeTemporaryForegrounds(); + final temporaryForegrounds = handler.createForegrounds( + controller, + document, + page, + blocState.info, + currentArea, + ); + if (handler.setupForegrounds) { + await Future.wait( + temporaryForegrounds.map( + (e) async => await e.setup( + controller.transformCubit, + document, + blocState.assetService, + page, + ), + ), + ); + } + setTemporaryTool( + handler: handler, + index: index, + foregrounds: temporaryForegrounds, + toolbar: await handler.getToolbar(bloc), + cursor: handler.cursor, + rendererStates: handler.rendererStates, + temporaryState: temporaryState, + ); + controller.rendererCubit.setRendererStates( + temporaryRendererStates: handler.rendererStates, + ); + await controller.rendererCubit.bake(controller, blocState); + } else if (selectState == SelectState.toggle && index != null) { + await toggleHandler(controller, bloc, index); + } + return handler; + } + + void resetReleaseHandler(DocumentBloc bloc, [RendererCubit? rendererCubit]) { + if (state.temporaryState == TemporaryState.removeAfterRelease) { + resetTemporaryHandler(bloc, true, rendererCubit); + } + } + + void resetDownHandler(DocumentBloc bloc, [RendererCubit? rendererCubit]) { + resetTemporaryHandler(bloc, false, rendererCubit); + } + + void resetTemporaryHandler( + DocumentBloc bloc, [ + bool force = false, + RendererCubit? rendererCubit, + ]) { + if (state.temporaryHandler == null) { + return; + } + if (!force && state.temporaryState != TemporaryState.removeAfterClick) { + if (state.temporaryState == TemporaryState.allowClick) { + setTemporaryState(TemporaryState.removeAfterClick); + } + return; + } + state.temporaryHandler?.dispose(bloc); + disposeTemporaryForegrounds(); + setTemporaryTool( + handler: null, + index: null, + foregrounds: null, + toolbar: null, + cursor: null, + rendererStates: null, + ); + rendererCubit?.setRendererStates(temporaryRendererStates: const {}); + } + + Future refresh( + EditorController controller, + DocumentLoaded blocState, { + bool allowBake = true, + }) async { + talker.verbose('Refreshing tools'); + final document = blocState.data; + final page = blocState.page; + final info = blocState.info; + final assetService = blocState.assetService; + final currentArea = blocState.currentArea; + const mapEq = MapEquality(); + if (!controller.isClosed) { + disposeAllForegrounds(); + final temporaryForegrounds = state.temporaryHandler?.createForegrounds( + controller, + document, + page, + info, + currentArea, + ); + if (temporaryForegrounds != null && + state.temporaryHandler?.setupForegrounds == true) { + await Future.wait( + temporaryForegrounds.map( + (e) async => await e.setup( + controller.transformCubit, + document, + assetService, + page, + ), + ), + ); + } + final foregrounds = state.handler.createForegrounds( + controller, + document, + page, + info, + currentArea, + ); + if (state.handler.setupForegrounds) { + await Future.wait( + foregrounds.map( + (e) async => await e.setup( + controller.transformCubit, + document, + assetService, + page, + ), + ), + ); + } + final toggleableForegrounds = >{}; + for (final entry in state.toggleableHandlers.entries) { + final handler = entry.value; + final index = entry.key; + final foregrounds = handler.createForegrounds( + controller, + document, + page, + info, + currentArea, + ); + if (handler.setupForegrounds) { + await Future.wait( + foregrounds.map( + (e) async => await e.setup( + controller.transformCubit, + document, + assetService, + page, + ), + ), + ); + } + toggleableForegrounds[index] = foregrounds; + } + final rendererStates = state.handler.rendererStates; + final temporaryRendererStates = state.temporaryHandler?.rendererStates; + final statesChanged = !mapEq.equals( + controller.rendererCubit.state.rendererStates, + rendererStates, + ); + final temporaryStatesChanged = !mapEq.equals( + controller.rendererCubit.state.temporaryRendererStates, + temporaryRendererStates, + ); + final shouldBake = statesChanged || temporaryStatesChanged; + setForegrounds( + temporaryForegrounds: temporaryForegrounds, + toggleableForegrounds: toggleableForegrounds, + foregrounds: foregrounds, + cursor: state.handler.cursor ?? MouseCursor.defer, + temporaryCursor: state.temporaryHandler?.cursor, + ); + controller.rendererCubit.setRendererStates( + rendererStates: statesChanged + ? rendererStates + : controller.rendererCubit.state.rendererStates, + temporaryRendererStates: temporaryStatesChanged + ? temporaryRendererStates + : controller.rendererCubit.state.temporaryRendererStates, + ); + if (allowBake) { + if (shouldBake) { + return controller.rendererCubit.bake( + controller, + blocState, + reset: true, + ); + } else if (!controller.rendererCubit.state.cameraViewport.baked) { + return controller.rendererCubit.delayedBake(controller, blocState); + } + } + } + } + + Future refreshToolbar(DocumentBloc bloc) async { + final toolbar = await state.handler.getToolbar(bloc); + final temporaryToolbar = await state.temporaryHandler?.getToolbar(bloc); + setToolbar(toolbar: toolbar, temporaryToolbar: temporaryToolbar); + } + + Future refreshForegrounds( + EditorController controller, + DocumentLoaded blocState, + ) => foregroundRefreshRunner.schedule( + () => _refreshForegrounds(controller, blocState), + ); + + Future _refreshForegrounds( + EditorController controller, + DocumentLoaded blocState, + ) async { + if (controller.isClosed) return; + final document = blocState.data; + final page = blocState.page; + final info = blocState.info; + final assetService = blocState.assetService; + final currentArea = blocState.currentArea; + + disposeForegrounds(); + disposeTemporaryForegrounds(); + + final temporaryForegrounds = state.temporaryHandler?.createForegrounds( + controller, + document, + page, + info, + currentArea, + ); + if (temporaryForegrounds != null && + temporaryForegrounds.isNotEmpty && + state.temporaryHandler?.setupForegrounds == true) { + await Future.wait( + temporaryForegrounds.map( + (e) async => await e.setup( + controller.transformCubit, + document, + assetService, + page, + ), + ), + ); + } + + final foregrounds = state.handler.createForegrounds( + controller, + document, + page, + info, + currentArea, + ); + if (foregrounds.isNotEmpty && state.handler.setupForegrounds) { + await Future.wait( + foregrounds.map( + (e) async => await e.setup( + controller.transformCubit, + document, + assetService, + page, + ), + ), + ); + } + + const mapEq = MapEquality(); + final rendererStates = state.handler.rendererStates; + final temporaryRendererStates = state.temporaryHandler?.rendererStates; + final statesChanged = !mapEq.equals( + controller.rendererCubit.state.rendererStates, + rendererStates, + ); + final temporaryStatesChanged = !mapEq.equals( + controller.rendererCubit.state.temporaryRendererStates, + temporaryRendererStates, + ); + + setForegrounds( + foregrounds: foregrounds, + temporaryForegrounds: temporaryForegrounds, + cursor: state.handler.cursor ?? MouseCursor.defer, + temporaryCursor: state.temporaryHandler?.cursor, + ); + controller.rendererCubit.setRendererStates( + rendererStates: statesChanged + ? rendererStates + : controller.rendererCubit.state.rendererStates, + temporaryRendererStates: temporaryStatesChanged + ? temporaryRendererStates + : controller.rendererCubit.state.temporaryRendererStates, + ); + + if (statesChanged || temporaryStatesChanged) { + await controller.rendererCubit.bake(controller, blocState, reset: true); + } + } + + void updateIndex(EditorController controller, DocumentBloc bloc) { + final docState = bloc.state; + if (docState is! DocumentLoadSuccess) return; + final info = docState.info; + final index = info.tools.indexOf(state.handler.data); + if (index < 0) { + changeTool(controller, bloc, index: state.index ?? 0); + } + if (index == state.index) { + return; + } + setIndex(index); + final selection = state.selection; + if (selection?.selected.contains(state.handler.data) ?? false) { + resetSelection(); + } + } + Future disposeRuntime(dynamic bloc) async { state.handler.dispose(bloc); state.temporaryHandler?.dispose(bloc); @@ -486,6 +2190,108 @@ class DocumentSaveCubit extends Cubit { ); void setDelayed(bool delayed) => emit(state.copyWith(isSaveDelayed: delayed)); + + ExternalStorage? getRemoteStorage() => + settingsCubit.getRemote(state.location.remote); + + bool hasAutosave(NetworkingService networkingService) => + settingsCubit.state.autosave && + (networkingService.isActive || + !(state.embedding?.save ?? true) || + (!kIsWeb && + !state.absolute && + (state.location.isEmpty || + (state.location.fileType?.isNote() ?? false)) && + (state.location.remote.isEmpty || + (settingsCubit + .getRemote(state.location.remote) + ?.hasDocumentCached(state.location.path) ?? + false)))); + + Future save( + DocumentBloc bloc, + NetworkingService networkingService, { + AssetLocation? location, + bool force = false, + bool isAutosave = false, + }) async { + final absolute = state.absolute; + if (location == null && + !force && + (state.saved == SaveState.saved || + state.saved == SaveState.absoluteRead)) { + return state.location; + } + if (networkingService.isClient) { + return AssetLocation.empty; + } + if (state.isSaveDelayed && isAutosave) { + return state.location; + } + final storage = getRemoteStorage(); + final fileSystem = bloc.state.fileSystem.buildDocumentSystem(storage); + final isDelayed = settingsCubit.state.delayedAutosave; + if (isDelayed && isAutosave) { + final seconds = max(0, settingsCubit.state.autosaveDelaySeconds); + setDelayed(true); + await Future.delayed(Duration(seconds: seconds)); + if (!state.isSaveDelayed) { + return state.location; + } + } + return savingLock.synchronized(() async { + if (location == null && + !force && + (state.saved == SaveState.saved || + state.saved == SaveState.absoluteRead)) { + return state.location; + } + var current = location ?? state.location; + if (isClosed) { + return current; + } + setSaveState(saved: SaveState.saving, location: current); + setDelayed(false); + final blocState = bloc.state; + final currentData = await blocState.saveData(); + if (isClosed) { + return current; + } + if (currentData == null || state.embedding != null) { + setSaveState(saved: SaveState.saved); + return AssetLocation.empty; + } + if (absolute || !(current.fileType?.isNote() ?? false)) { + final file = await compute(_toFile, (currentData, false)); + final document = await fileSystem.createFileWithName( + name: currentData.name, + suffix: '.bfly', + directory: absolute + ? null + : current.fileExtension.isEmpty + ? state.location.path + : state.location.parent, + file, + ); + current = document.location; + } else { + final file = await compute(_toFile, ( + currentData, + current.fileType == AssetFileType.textNote, + )); + await fileSystem.updateFile(current.path, file); + } + settingsCubit.addRecentHistory(current); + if (isClosed) { + return current; + } + setSaveState( + saved: state.saved == SaveState.saving ? SaveState.saved : state.saved, + location: current, + ); + return current; + }); + } } @freezed diff --git a/app/lib/cubits/transform.dart b/app/lib/cubits/transform.dart index 2f4e0ab78e76..4d13765394df 100644 --- a/app/lib/cubits/transform.dart +++ b/app/lib/cubits/transform.dart @@ -1,12 +1,18 @@ import 'dart:math'; +import 'package:butterfly/bloc/document_bloc.dart'; +import 'package:butterfly/cubits/editor_runtime.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/helpers/point.dart'; import 'package:butterfly/helpers/rect.dart'; +import 'package:butterfly/views/navigator/constants.dart'; import 'package:butterfly_api/butterfly_api.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/physics.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:material_leap/material_leap.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'transform.freezed.dart'; @@ -189,4 +195,466 @@ class TransformCubit extends Cubit { positionBounds: positionBounds, ), ); + + Rect getContentRect(RendererCubit rendererCubit, [Area? currentArea]) { + if (currentArea != null) { + return currentArea.rect; + } + final renderers = rendererCubit.renderers; + if (renderers.isEmpty) { + return Rect.zero; + } + + var minX = double.infinity; + var minY = double.infinity; + var maxX = double.negativeInfinity; + var maxY = double.negativeInfinity; + + for (final renderer in renderers) { + final rect = renderer.expandedRect; + if (rect != null) { + minX = min(minX, rect.left); + minY = min(minY, rect.top); + maxX = max(maxX, rect.right); + maxY = max(maxY, rect.bottom); + } + } + + if (minX == double.infinity) { + return Rect.zero; + } + + return Rect.fromLTRB(minX, minY, maxX, maxY); + } + + bool _isNavigationRailVisible( + SettingsCubit settingsCubit, + RendererCubit rendererCubit, + EditorInputCubit inputCubit, + ) { + final settings = settingsCubit.state; + final viewport = rendererCubit.state.cameraViewport; + return settings.navigationRail && + settings.navigatorPosition == NavigatorPosition.left && + inputCubit.state.hideUi == HideState.visible && + (viewport.width ?? 0) >= LeapBreakpoints.expanded && + (viewport.height ?? 0) >= 400; + } + + Rect? calculateViewportBounds({ + required SettingsCubit settingsCubit, + required RendererCubit rendererCubit, + required EditorInputCubit inputCubit, + Area? currentArea, + CameraTransform? customTransform, + }) { + final settings = settingsCubit.state; + var multiplier = settings.limitViewportMultiplier; + final positive = settings.limitViewportPositive; + + if (multiplier == null && !positive && currentArea == null) return null; + + final viewport = rendererCubit.state.cameraViewport; + final transform = customTransform ?? state; + final navigationRailOffset = + _isNavigationRailVisible(settingsCubit, rendererCubit, inputCubit) + ? kNavigationRailWidth / transform.size + : 0.0; + final size = + Size( + ((viewport.width ?? 0) / transform.size) - navigationRailOffset, + (viewport.height ?? 0) / transform.size, + ) / + settings.renderResolution.multiplier; + + final contentRect = getContentRect(rendererCubit, currentArea); + + double minX = double.negativeInfinity; + double minY = double.negativeInfinity; + double maxX = double.infinity; + double maxY = double.infinity; + + if (multiplier != null || currentArea != null) { + multiplier ??= 1; + final padX = size.width * multiplier; + final padY = size.height * multiplier; + + minX = contentRect.left - padX; + minY = contentRect.top - padY; + maxX = contentRect.right - size.width + padX; + maxY = contentRect.bottom - size.height + padY; + + if (minX > maxX) { + final mid = (minX + maxX) / 2; + minX = mid; + maxX = mid; + } + if (minY > maxY) { + final mid = (minY + maxY) / 2; + minY = mid; + maxY = mid; + } + } + + if (positive && currentArea == null) { + minX = max(-navigationRailOffset, minX); + minY = max(0.0, minY); + maxX = max(-navigationRailOffset, maxX); + maxY = max(0.0, maxY); + } + + return Rect.fromLTRB(minX, minY, maxX, maxY); + } + + CameraTransform _clampTransform({ + required CameraTransform transform, + required SettingsCubit settingsCubit, + required RendererCubit rendererCubit, + required EditorInputCubit inputCubit, + }) { + final bounds = calculateViewportBounds( + settingsCubit: settingsCubit, + rendererCubit: rendererCubit, + inputCubit: inputCubit, + customTransform: transform, + ); + if (bounds == null) return transform; + return transform.withPosition( + Offset( + transform.position.dx.clamp(bounds.left, bounds.right), + transform.position.dy.clamp(bounds.top, bounds.bottom), + ), + ); + } + + Area? getRelativeArea({ + required DocumentLoadSuccess docState, + required EditorViewCubit viewCubit, + required Area currentArea, + required int dx, + required int dy, + bool? exact, + }) { + if (dx == 0 && dy == 0) return null; + + final rect = currentArea.rect.translate( + dx.toDouble() * currentArea.rect.width, + dy.toDouble() * currentArea.rect.height, + ); + + return docState.page.areas.firstWhereOrNull((area) { + final currentAreaRect = area.rect; + if (exact ?? viewCubit.state.areaNavigatorExact) { + return (currentAreaRect.top - rect.top).abs() < + precisionErrorTolerance && + (currentAreaRect.left - rect.left).abs() < + precisionErrorTolerance && + (currentAreaRect.width - rect.width).abs() < + precisionErrorTolerance && + (currentAreaRect.height - rect.height).abs() < + precisionErrorTolerance; + } + return currentAreaRect.overlaps(rect.deflate(precisionErrorTolerance)); + }); + } + + void teleportToAreaEdge({ + required Area area, + required int dx, + required int dy, + required SettingsCubit settingsCubit, + required RendererCubit rendererCubit, + required EditorInputCubit inputCubit, + }) { + final newBounds = calculateViewportBounds( + settingsCubit: settingsCubit, + rendererCubit: rendererCubit, + inputCubit: inputCubit, + currentArea: area, + ); + if (newBounds == null) return; + + final pos = state.position; + double newX = pos.dx; + double newY = pos.dy; + if (dx > 0) { + newX = newBounds.left; + } else if (dx < 0) { + newX = newBounds.right; + } else { + newX = newX.clamp(newBounds.left, newBounds.right); + } + if (dy > 0) { + newY = newBounds.top; + } else if (dy < 0) { + newY = newBounds.bottom; + } else { + newY = newY.clamp(newBounds.top, newBounds.bottom); + } + teleport(Offset(newX, newY)); + } + + Future navigateToRelativeArea({ + required DocumentBloc bloc, + required SettingsCubit settingsCubit, + required RendererCubit rendererCubit, + required EditorInputCubit inputCubit, + required EditorViewCubit viewCubit, + required int dx, + required int dy, + Future Function()? createAreaName, + }) async { + final docState = bloc.state; + if (docState is! DocumentLoadSuccess) return; + + final current = docState.currentArea; + if (current == null) return; + + var area = getRelativeArea( + docState: docState, + viewCubit: viewCubit, + currentArea: current, + dx: dx, + dy: dy, + ); + if (area != null) { + bloc.add(CurrentAreaChanged(area.name)); + teleportToAreaEdge( + area: area, + dx: dx, + dy: dy, + settingsCubit: settingsCubit, + rendererCubit: rendererCubit, + inputCubit: inputCubit, + ); + return; + } + + if (!viewCubit.state.areaNavigatorCreate || createAreaName == null) return; + final name = await createAreaName(); + if (name == null) return; + + final rect = current.rect.translate( + dx.toDouble() * current.rect.width, + dy.toDouble() * current.rect.height, + ); + + final newArea = Area( + position: rect.topLeft.toPoint(), + height: rect.height, + width: rect.width, + name: name, + ); + bloc.add(AreasCreated([AreaPreset(area: newArea)])); + bloc.add(CurrentAreaChanged(name)); + teleportToAreaEdge( + area: newArea, + dx: dx, + dy: dy, + settingsCubit: settingsCubit, + rendererCubit: rendererCubit, + inputCubit: inputCubit, + ); + } + + void moveConstrained( + Offset delta, { + required SettingsCubit settingsCubit, + required RendererCubit rendererCubit, + required EditorInputCubit inputCubit, + required EditorViewCubit viewCubit, + DocumentBloc? bloc, + bool force = false, + Area? currentArea, + }) { + final utilitiesState = viewCubit.state.utilities; + if (!force) { + if (utilitiesState.lockHorizontal) delta = Offset(0, delta.dy); + if (utilitiesState.lockVertical) delta = Offset(delta.dx, 0); + + final bounds = calculateViewportBounds( + settingsCubit: settingsCubit, + rendererCubit: rendererCubit, + inputCubit: inputCubit, + currentArea: currentArea, + ); + if (bounds != null) { + final pos = state.position; + var newPos = pos + delta; + final clampedPos = Offset( + newPos.dx.clamp(bounds.left, bounds.right), + newPos.dy.clamp(bounds.top, bounds.bottom), + ); + + final docState = bloc?.state; + if (currentArea != null && + docState is DocumentLoadSuccess && + (clampedPos.dx != newPos.dx || clampedPos.dy != newPos.dy)) { + int dx = 0; + int dy = 0; + if (newPos.dx < bounds.left) { + dx = -1; + } else if (newPos.dx > bounds.right) { + dx = 1; + } + + if (newPos.dy < bounds.top) { + dy = -1; + } else if (newPos.dy > bounds.bottom) { + dy = 1; + } + + if ((dx != 0 || dy != 0) && + settingsCubit.state.hasFlag('edgePanAreaSwitching')) { + final area = getRelativeArea( + docState: docState, + viewCubit: viewCubit, + currentArea: currentArea, + dx: dx, + dy: dy, + ); + if (area != null) { + bloc?.add(CurrentAreaChanged(area.name)); + teleportToAreaEdge( + area: area, + dx: dx, + dy: dy, + settingsCubit: settingsCubit, + rendererCubit: rendererCubit, + inputCubit: inputCubit, + ); + return; + } + } + } + + delta = clampedPos - pos; + } + } + + if (delta.dx == 0 && delta.dy == 0) { + return; + } + move(delta); + } + + void zoomConstrained( + double delta, { + required SettingsCubit settingsCubit, + required RendererCubit rendererCubit, + required EditorInputCubit inputCubit, + required EditorViewCubit viewCubit, + Offset cursor = Offset.zero, + bool force = false, + }) { + final utilitiesState = viewCubit.state.utilities; + if (utilitiesState.lockZoom && !force) { + delta = 1; + } + if (delta == 1) { + return; + } + if (force) { + zoom(delta, cursor); + return; + } + final transform = state.withSize(state.size * delta, cursor); + final clamped = _clampTransform( + transform: transform, + settingsCubit: settingsCubit, + rendererCubit: rendererCubit, + inputCubit: inputCubit, + ); + teleport(clamped.position, clamped.size); + } + + void sizeConstrained( + double size, { + required SettingsCubit settingsCubit, + required RendererCubit rendererCubit, + required EditorInputCubit inputCubit, + required EditorViewCubit viewCubit, + Offset cursor = Offset.zero, + bool force = false, + }) { + final utilitiesState = viewCubit.state.utilities; + if (utilitiesState.lockZoom && !force) return; + if (force) { + this.size(size, cursor); + return; + } + final transform = _clampTransform( + transform: state.withSize(size, cursor), + settingsCubit: settingsCubit, + rendererCubit: rendererCubit, + inputCubit: inputCubit, + ); + teleport(transform.position, transform.size); + } + + void slideConstrained( + Offset positionVelocity, + double sizeVelocity, { + required SettingsCubit settingsCubit, + required RendererCubit rendererCubit, + required EditorInputCubit inputCubit, + required EditorViewCubit viewCubit, + bool force = false, + Area? currentArea, + }) { + final settings = settingsCubit.state; + if (!settings.hasFlag('smoothNavigation')) return; + final utilitiesState = viewCubit.state.utilities; + Rect? bounds; + var outOfBounds = false; + if (!force) { + if (utilitiesState.lockHorizontal) { + positionVelocity = Offset(0, positionVelocity.dy); + } + if (utilitiesState.lockVertical) { + positionVelocity = Offset(positionVelocity.dx, 0); + } + if (utilitiesState.lockZoom) sizeVelocity = 0; + + bounds = calculateViewportBounds( + settingsCubit: settingsCubit, + rendererCubit: rendererCubit, + inputCubit: inputCubit, + currentArea: currentArea, + ); + if (bounds != null) { + final pos = state.position; + final clampedPos = Offset( + pos.dx.clamp(bounds.left, bounds.right), + pos.dy.clamp(bounds.top, bounds.bottom), + ); + outOfBounds = clampedPos != pos; + var vX = positionVelocity.dx; + var vY = positionVelocity.dy; + + if (pos.dx >= bounds.right && vX > 0) { + vX = 0; + } + if (pos.dx <= bounds.left && vX < 0) { + vX = 0; + } + if (pos.dy >= bounds.bottom && vY > 0) { + vY = 0; + } + if (pos.dy <= bounds.top && vY < 0) { + vY = 0; + } + + positionVelocity = Offset(vX, vY); + } + } + + if (positionVelocity.dx == 0 && + positionVelocity.dy == 0 && + sizeVelocity == 0 && + !outOfBounds) { + return; + } + slide(positionVelocity, sizeVelocity, positionBounds: bounds); + } } diff --git a/app/lib/dialogs/collections.dart b/app/lib/dialogs/collections.dart index 5601239424c9..317e42dae1aa 100644 --- a/app/lib/dialogs/collections.dart +++ b/app/lib/dialogs/collections.dart @@ -98,9 +98,14 @@ class _CollectionsDialogState extends State { Navigator.pop(context); final cubit = bloc.editorController; final handler = - cubit.fetchHandler() ?? - await cubit.changeTemporaryHandler( + cubit.toolCubit.fetchHandler( + editable: + cubit.saveCubit.state.embedding?.editable != + false, + ) ?? + await cubit.toolCubit.changeTemporaryHandler( context, + cubit, SelectTool(), bloc: bloc, temporaryState: TemporaryState.removeAfterClick, diff --git a/app/lib/dialogs/elements.dart b/app/lib/dialogs/elements.dart index 95b68b0afa9c..9791b2037c91 100644 --- a/app/lib/dialogs/elements.dart +++ b/app/lib/dialogs/elements.dart @@ -12,7 +12,6 @@ import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../../renderers/renderer.dart'; import '../bloc/document_bloc.dart'; -import '../cubits/editor_controller.dart'; import '../services/import.dart'; ContextMenuBuilder buildElementsContextMenu( @@ -58,11 +57,11 @@ ContextMenuBuilder buildElementsContextMenu( ContextMenuItem( onPressed: () { Navigator.of(context).pop(true); - cubit.fetchHandler()?.copySelection( - bloc, - clipboardManager, - true, - ); + cubit.toolCubit + .fetchHandler( + editable: cubit.saveCubit.state.embedding?.editable != false, + ) + ?.copySelection(bloc, clipboardManager, true); }, icon: const PhosphorIcon(PhosphorIconsLight.scissors), label: AppLocalizations.of(context).cut, @@ -70,11 +69,11 @@ ContextMenuBuilder buildElementsContextMenu( ContextMenuItem( onPressed: () { Navigator.of(context).pop(true); - cubit.fetchHandler()?.copySelection( - bloc, - clipboardManager, - false, - ); + cubit.toolCubit + .fetchHandler( + editable: cubit.saveCubit.state.embedding?.editable != false, + ) + ?.copySelection(bloc, clipboardManager, false); }, icon: const PhosphorIcon(PhosphorIconsLight.copy), label: AppLocalizations.of(context).copy, @@ -97,12 +96,11 @@ ContextMenuBuilder buildElementsContextMenu( page, ); } - cubit.fetchHandler()?.transform( - bloc, - null, - next: transforms, - duplicate: true, - ); + cubit.toolCubit + .fetchHandler( + editable: cubit.saveCubit.state.embedding?.editable != false, + ) + ?.transform(bloc, null, next: transforms, duplicate: true); }, label: AppLocalizations.of(context).duplicate, ), @@ -180,7 +178,9 @@ ContextMenuBuilder buildElementsContextMenu( Navigator.of(context).pop(true); if (renderers.isEmpty) return; cubit.toolCubit.changeSelection(renderers.first); - renderers.sublist(1).forEach((r) => cubit.toolCubit.insertSelection(r)); + renderers + .sublist(1) + .forEach((r) => cubit.toolCubit.insertSelection(r)); }, icon: const PhosphorIcon(PhosphorIconsLight.faders), label: AppLocalizations.of(context).properties, diff --git a/app/lib/dialogs/export/general.dart b/app/lib/dialogs/export/general.dart index 02bb5dac43d9..fc416e336f0c 100644 --- a/app/lib/dialogs/export/general.dart +++ b/app/lib/dialogs/export/general.dart @@ -2,7 +2,6 @@ import 'dart:math'; import 'package:butterfly/api/save.dart'; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter/foundation.dart'; @@ -99,7 +98,8 @@ class _GeneralExportDialogState extends State { ); } } - return bloc.editorController.render( + return bloc.editorController.rendererCubit.render( + bloc.editorController, state.data, state.page, state.info, @@ -113,7 +113,7 @@ class _GeneralExportDialogState extends State { final bloc = context.read(); final state = bloc.state; if (state is! DocumentLoaded) return null; - return bloc.editorController + return bloc.editorController.rendererCubit .renderSVG( state.data, state.page, @@ -310,7 +310,7 @@ class _GeneralExportDialogState extends State { final state = bloc.state; if (state is! DocumentLoaded) return; final cubit = bloc.editorController; - final rect = cubit.getPageRect( + final rect = cubit.rendererCubit.getPageRect( invisibleLayers: state.invisibleLayers, ); setState(() { diff --git a/app/lib/dialogs/export/pdf.dart b/app/lib/dialogs/export/pdf.dart index 62aeb85025dc..8c164568e614 100644 --- a/app/lib/dialogs/export/pdf.dart +++ b/app/lib/dialogs/export/pdf.dart @@ -219,7 +219,9 @@ class _PdfExportDialogState extends State { if (state is! DocumentLoadSuccess) return; final loading = showLoadingDialog(context); try { - final pdf = await context.read().editorController.renderPDF( + final editorController = context.read().editorController; + final pdf = await editorController.rendererCubit.renderPDF( + editorController, state, areas: _areas.map((e) => e.preset).toList(), onProgress: (progress) => loading?.setProgress(progress), @@ -429,7 +431,8 @@ class _AreaPreviewState extends State<_AreaPreview> { void _load() { const maxImageDimension = 1000; final maxSide = max(widget.area.width, widget.area.height); - _future = widget.currentIndex.render( + _future = widget.currentIndex.rendererCubit.render( + widget.currentIndex, widget.state.data, widget.page, widget.state.info, diff --git a/app/lib/dialogs/export/thumbnail.dart b/app/lib/dialogs/export/thumbnail.dart index 45a52d0e348d..f4cc23c7c2b8 100644 --- a/app/lib/dialogs/export/thumbnail.dart +++ b/app/lib/dialogs/export/thumbnail.dart @@ -1,7 +1,6 @@ import 'dart:math'; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/view_painter.dart'; import 'package:butterfly_api/butterfly_api.dart'; @@ -513,7 +512,8 @@ class _ThumbnailCaptureDialogState extends State { ); final editorController = context.read().editorController; - final thumbnail = await editorController.render( + final thumbnail = await editorController.rendererCubit.render( + editorController, widget.state.data, widget.state.page, widget.state.info, diff --git a/app/lib/dialogs/import/add.dart b/app/lib/dialogs/import/add.dart index f51ac6afe92c..1a8d0a202853 100644 --- a/app/lib/dialogs/import/add.dart +++ b/app/lib/dialogs/import/add.dart @@ -287,7 +287,8 @@ class _AddDialogState extends State { bloc.add(ToolCreated(defaultTool)); if (!defaultTool.isAction()) { - editorController.changeTool( + editorController.toolCubit.changeTool( + editorController, bloc, index: state.info.tools.length, context: context, diff --git a/app/lib/dialogs/packs/asset.dart b/app/lib/dialogs/packs/asset.dart index c29a2da1915f..b96147ad03d0 100644 --- a/app/lib/dialogs/packs/asset.dart +++ b/app/lib/dialogs/packs/asset.dart @@ -12,7 +12,6 @@ import 'package:material_leap/l10n/leap_localizations.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../../bloc/document_bloc.dart'; -import '../../cubits/editor_controller.dart'; import 'pack.dart'; class AssetDialog extends StatelessWidget { @@ -147,7 +146,8 @@ Future addToPack( if (result == null) return; var pack = await packSystem.getFile(result.namespace); if (pack == null) return; - final screenshot = await bloc.editorController.render( + final screenshot = await bloc.editorController.rendererCubit.render( + bloc.editorController, state.data, state.page, state.info, diff --git a/app/lib/embed/handler.dart b/app/lib/embed/handler.dart index 3d60412c6e4a..3468c15724b5 100644 --- a/app/lib/embed/handler.dart +++ b/app/lib/embed/handler.dart @@ -137,7 +137,8 @@ class EmbedHandler { scale = _mapDouble(map, 'scale', 1); renderBackground = _mapBool(map, 'renderBackground', true); } - final data = await bloc.editorController.render( + final data = await bloc.editorController.rendererCubit.render( + bloc.editorController, state.data, state.page, state.info, @@ -173,7 +174,7 @@ class EmbedHandler { } sendEmbedMessage( 'renderSVG', - bloc.editorController + bloc.editorController.rendererCubit .renderSVG( state.data, state.page, diff --git a/app/lib/handlers/eye_dropper.dart b/app/lib/handlers/eye_dropper.dart index c39e1a11dc57..8063301688e0 100644 --- a/app/lib/handlers/eye_dropper.dart +++ b/app/lib/handlers/eye_dropper.dart @@ -9,8 +9,10 @@ class EyeDropperHandler extends Handler { bool wasAdded = true, ]) { if (!wasAdded) { - context.read().changeTemporaryHandler( + final editorController = context.read(); + editorController.toolCubit.changeTemporaryHandler( context, + editorController, data, temporaryState: TemporaryState.removeAfterRelease, ); @@ -25,7 +27,9 @@ class EyeDropperHandler extends Handler { ); final state = context.getState(); if (state == null) return; - final data = await context.getEditorController().render( + final editorController = context.getEditorController(); + final data = await editorController.rendererCubit.render( + editorController, state.data, state.page, state.info, @@ -36,7 +40,10 @@ class EyeDropperHandler extends Handler { final image = img.decodePng(data.buffer.asUint8List()); if (image == null) return; final pixel = image.getPixel(0, 0); - final handler = context.getEditorController().getHandler( + final handler = context.getEditorController().toolCubit.getHandler( + editable: + context.getEditorController().saveCubit.state.embedding?.editable != + false, disableTemporary: true, ); final color = SRGBColor.from( diff --git a/app/lib/handlers/mixins.dart b/app/lib/handlers/mixins.dart index f81f584df9b3..c7d1357f3ec5 100644 --- a/app/lib/handlers/mixins.dart +++ b/app/lib/handlers/mixins.dart @@ -11,7 +11,9 @@ mixin ColoredHandler on Handler { void changeStartedDrawing(EventContext context) { if (_startedDrawing) return; _startedDrawing = true; - context.getEditorController().refreshToolbar(context.getDocumentBloc()); + context.getEditorController().toolCubit.refreshToolbar( + context.getDocumentBloc(), + ); } @override @@ -25,8 +27,9 @@ mixin ColoredHandler on Handler { color: getColor(), onChanged: (value) => changeToolColor(bloc, value), onEyeDropper: (context) { - bloc.editorController.changeTemporaryHandler( + bloc.editorController.toolCubit.changeTemporaryHandler( context, + bloc.editorController, EyeDropperTool(), bloc: bloc, temporaryState: TemporaryState.removeAfterRelease, diff --git a/app/lib/handlers/pen.dart b/app/lib/handlers/pen.dart index b629e5f5b92a..cbfeed28107e 100644 --- a/app/lib/handlers/pen.dart +++ b/app/lib/handlers/pen.dart @@ -203,7 +203,7 @@ class PenHandler extends Handler with ColoredHandler { @override void onPointerDown(PointerDownEvent event, EventContext context) { final cubit = context.getEditorController(); - cubit.cancelDelayedBake(); + cubit.rendererCubit.cancelDelayedBake(); isDrawing = true; changeStartedDrawing(context); _hideCursorWhileDrawing = context.getSettings().hideCursorWhileDrawing; diff --git a/app/lib/handlers/polygon.dart b/app/lib/handlers/polygon.dart index d986711da1c6..0a9ddf86066c 100644 --- a/app/lib/handlers/polygon.dart +++ b/app/lib/handlers/polygon.dart @@ -404,7 +404,11 @@ class PolygonHandler extends Handler with ColoredHandler { bloc.add(ElementsCreated([element])); } _resetTool(); - bloc.editorController.resetTemporaryHandler(bloc, true); + bloc.editorController.toolCubit.resetTemporaryHandler( + bloc, + true, + bloc.editorController.rendererCubit, + ); bloc.delayedBake(); bloc.refreshToolbar(); } @@ -425,7 +429,11 @@ class PolygonHandler extends Handler with ColoredHandler { _resetTool(); bloc.refreshForegrounds(); bloc.refreshToolbar(); - bloc.editorController.resetTemporaryHandler(bloc, true); + bloc.editorController.toolCubit.resetTemporaryHandler( + bloc, + true, + bloc.editorController.rendererCubit, + ); return; } @@ -438,7 +446,11 @@ class PolygonHandler extends Handler with ColoredHandler { bloc.refresh(); } _resetTool(); - bloc.editorController.resetTemporaryHandler(bloc, true); + bloc.editorController.toolCubit.resetTemporaryHandler( + bloc, + true, + bloc.editorController.rendererCubit, + ); } else { _selectedPointIndex = max(0, selectedIndex - 1); } diff --git a/app/lib/handlers/presentation.dart b/app/lib/handlers/presentation.dart index e9569628c3a1..03d39364d68e 100644 --- a/app/lib/handlers/presentation.dart +++ b/app/lib/handlers/presentation.dart @@ -226,7 +226,7 @@ class PresentationHandler extends GeneralHandHandler void _refreshToolbar(DocumentBloc bloc) { final state = bloc.state; if (state is! DocumentLoaded) return; - bloc.editorController.refreshToolbar(bloc); + bloc.editorController.toolCubit.refreshToolbar(bloc); } } diff --git a/app/lib/handlers/select.dart b/app/lib/handlers/select.dart index 1cf1b10c0e44..c490c058a63c 100644 --- a/app/lib/handlers/select.dart +++ b/app/lib/handlers/select.dart @@ -564,7 +564,9 @@ class SelectHandler extends Handler { if (state is! DocumentLoadSuccess) return; _selected.clear(); _selected.addAll( - bloc.editorController.rendererCubit.renderers.where((e) => filter?.call(e) ?? true), + bloc.editorController.rendererCubit.renderers.where( + (e) => filter?.call(e) ?? true, + ), ); _updateSelectionRect(); bloc.refreshForegrounds(); diff --git a/app/lib/handlers/shape.dart b/app/lib/handlers/shape.dart index c7cc6d9cc72c..e030b8192da3 100644 --- a/app/lib/handlers/shape.dart +++ b/app/lib/handlers/shape.dart @@ -42,7 +42,9 @@ class ShapeHandler extends PastingHandler with ColoredHandler { property: data.property.copyWith( strokeWidth: data.property.strokeWidth / - (data.zoomDependent ? cubit.rendererCubit.state.cameraViewport.scale : 1), + (data.zoomDependent + ? cubit.rendererCubit.state.cameraViewport.scale + : 1), ), collection: collection, ), diff --git a/app/lib/renderers/elements/polygon.dart b/app/lib/renderers/elements/polygon.dart index f6b742495d72..e91a2f39a9a9 100644 --- a/app/lib/renderers/elements/polygon.dart +++ b/app/lib/renderers/elements/polygon.dart @@ -164,12 +164,17 @@ class PolygonRenderer extends Renderer { ContextMenuItem? getContextMenuItem(DocumentBloc bloc, BuildContext context) { return ContextMenuItem( onPressed: () async { - bloc.editorController.fetchHandler()?.clearSelection( - bloc, - ); + bloc.editorController.toolCubit + .fetchHandler( + editable: + bloc.editorController.saveCubit.state.embedding?.editable != + false, + ) + ?.clearSelection(bloc); final polygon = - await bloc.editorController.changeTemporaryHandler( + await bloc.editorController.toolCubit.changeTemporaryHandler( context, + bloc.editorController, PolygonTool(property: element.property), bloc: bloc, ) diff --git a/app/lib/selections/document.dart b/app/lib/selections/document.dart index 913b423d0f0d..8bdc30a7f6fd 100644 --- a/app/lib/selections/document.dart +++ b/app/lib/selections/document.dart @@ -172,7 +172,8 @@ class _UtilitiesViewState extends State<_UtilitiesView> final heightOffset = (rect.height - captureHeight) / 2; final quality = kThumbnailWidth / (captureWidth * viewport.scale); - final thumbnail = await cubit.render( + final thumbnail = await cubit.rendererCubit.render( + cubit, state.data, state.page, state.info, @@ -413,9 +414,14 @@ class _UtilitiesViewState extends State<_UtilitiesView> .state .cameraViewport .toSize(); - context.read().size( + final editorController = context.read(); + editorController.transformCubit.sizeConstrained( value / 100, - Offset(size.width / 2, size.height / 2), + cursor: Offset(size.width / 2, size.height / 2), + settingsCubit: editorController.settingsCubit, + rendererCubit: editorController.rendererCubit, + inputCubit: editorController.inputCubit, + viewCubit: editorController.viewCubit, ); context.read().bake(); }, diff --git a/app/lib/services/import.dart b/app/lib/services/import.dart index 1b0a90b35f78..464b820bd223 100644 --- a/app/lib/services/import.dart +++ b/app/lib/services/import.dart @@ -159,8 +159,9 @@ class ImportResult { if (choosePosition && state != null && (elements.isNotEmpty || areas.isNotEmpty)) { - service.editorController?.changeTemporaryHandler( + service.editorController?.toolCubit.changeTemporaryHandler( context, + service.editorController!, ImportTool(elements: elements, areas: areas, assets: assets), bloc: bloc!, temporaryState: TemporaryState.removeAfterRelease, diff --git a/app/lib/services/network.dart b/app/lib/services/network.dart index b73d41e0f5e0..e53a669d2106 100644 --- a/app/lib/services/network.dart +++ b/app/lib/services/network.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'dart:math'; import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:flutter/foundation.dart'; diff --git a/app/lib/views/app_bar.dart b/app/lib/views/app_bar.dart index fa34c2be58a8..25fe41038148 100644 --- a/app/lib/views/app_bar.dart +++ b/app/lib/views/app_bar.dart @@ -281,8 +281,9 @@ class _AppBarTitleState extends State<_AppBarTitle> { final newLocation = location.copyWith( path: toFilePath(value), ); - final savedLocation = await cubit.save( + final savedLocation = await cubit.saveCubit.save( bloc, + cubit.networkingService, location: newLocation, force: true, ); @@ -394,7 +395,8 @@ class _AppBarTitleState extends State<_AppBarTitle> { ), const SizedBox(width: 8), if (state is DocumentLoadSuccess) ...[ - if ((!cubit.hasAutosave() || settings.showSaveButton) && + if ((!cubit.saveCubit.hasAutosave(cubit.networkingService) || + settings.showSaveButton) && currentIndex.embedding?.save != false) SizedBox( width: 42, diff --git a/app/lib/views/edit.dart b/app/lib/views/edit.dart index 35ef1b9e28c0..bdbbce54f4ca 100644 --- a/app/lib/views/edit.dart +++ b/app/lib/views/edit.dart @@ -173,7 +173,8 @@ class _EditToolbarState extends State { false, icon: _buildIcon(icon, size), selectedIcon: _buildIcon(iconFilled, size), - onLongPressed: () => cubit.toolCubit.changeSelection(tempData), + onLongPressed: () => + cubit.toolCubit.changeSelection(tempData), onPressed: () { if (_mouseState == _MouseState.multi) { cubit.toolCubit.insertSelection(tempData, true); @@ -299,95 +300,105 @@ class _EditToolbarState extends State { enabled: selected || highlighted, child: Builder( builder: (context) { - return cubit.useHandler(bloc, i, (handler) { - String tooltip = tool.name.trim(); - if (tooltip.isEmpty) { - tooltip = tool.getLocalizedName(context); - } - final status = handler.getStatus(bloc); - final theme = Theme.of(context); - final color = switch (status) { - ToolStatus.normal => null, - ToolStatus.disabled => theme.disabledColor, - ToolStatus.selected => theme.colorScheme.secondary, - }; - var handlerIcon = handler.getIcon(bloc); - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 4.0, - ), - child: OptionButton( - tooltip: tooltip, - onLongPressed: selected || highlighted - ? null - : () => context - .read() - .insertSelection(tool, true), - onDoubleTap: highlighted || selected - ? () => context - .read() - .insertSelection(tool, true) - : null, - onSecondaryPressed: () => context - .read() - .changeSelection(tool), - focussed: shortcuts.contains(InputMapping(i)), - selected: - selected || - currentIndex.toggleableHandlers.containsKey( - i, - ), - showBottom: selected || tool.isAction(), - highlighted: highlighted, - bottomIcon: selected || tool.isAction() - ? PhosphorIcon( - tool.isAction() - ? PhosphorIconsLight.playCircle - : isMobile - ? PhosphorIconsLight.caretUp - : switch (settings.toolbarPosition) { - ToolbarPosition.top || - ToolbarPosition.inline => - PhosphorIconsLight.caretDown, - ToolbarPosition.bottom => - PhosphorIconsLight.caretUp, - ToolbarPosition.left => - PhosphorIconsLight.caretRight, - ToolbarPosition.right => - PhosphorIconsLight.caretLeft, - }, - ) - : null, - selectedIcon: _buildIcon( - handlerIcon ?? - tool.icon(PhosphorIconsStyle.fill), - size, - color, + return cubit.toolCubit.useHandler( + bloc, + i, + (handler) { + String tooltip = tool.name.trim(); + if (tooltip.isEmpty) { + tooltip = tool.getLocalizedName(context); + } + final status = handler.getStatus(bloc); + final theme = Theme.of(context); + final color = switch (status) { + ToolStatus.normal => null, + ToolStatus.disabled => theme.disabledColor, + ToolStatus.selected => + theme.colorScheme.secondary, + }; + var handlerIcon = handler.getIcon(bloc); + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4.0, ), - icon: _buildIcon( - handlerIcon ?? - tool.icon(PhosphorIconsStyle.light), - size, - color, + child: OptionButton( + tooltip: tooltip, + onLongPressed: selected || highlighted + ? null + : () => context + .read() + .insertSelection(tool, true), + onDoubleTap: highlighted || selected + ? () => context + .read() + .insertSelection(tool, true) + : null, + onSecondaryPressed: () => context + .read() + .changeSelection(tool), + focussed: shortcuts.contains(InputMapping(i)), + selected: + selected || + currentIndex.toggleableHandlers.containsKey( + i, + ), + showBottom: selected || tool.isAction(), + highlighted: highlighted, + bottomIcon: selected || tool.isAction() + ? PhosphorIcon( + tool.isAction() + ? PhosphorIconsLight.playCircle + : isMobile + ? PhosphorIconsLight.caretUp + : switch (settings + .toolbarPosition) { + ToolbarPosition.top || + ToolbarPosition.inline => + PhosphorIconsLight.caretDown, + ToolbarPosition.bottom => + PhosphorIconsLight.caretUp, + ToolbarPosition.left => + PhosphorIconsLight.caretRight, + ToolbarPosition.right => + PhosphorIconsLight.caretLeft, + }, + ) + : null, + selectedIcon: _buildIcon( + handlerIcon ?? + tool.icon(PhosphorIconsStyle.fill), + size, + color, + ), + icon: _buildIcon( + handlerIcon ?? + tool.icon(PhosphorIconsStyle.light), + size, + color, + ), + onPressed: () { + if (_mouseState == _MouseState.multi) { + cubit.toolCubit.insertSelection(tool, true); + } else if (!selected || temp != null) { + cubit.toolCubit.resetSelection(); + cubit.toolCubit.changeTool( + cubit, + bloc, + index: i, + handler: handler, + context: context, + ); + } else { + cubit.toolCubit.changeSelection(tool, true); + } + }, ), - onPressed: () { - if (_mouseState == _MouseState.multi) { - cubit.toolCubit.insertSelection(tool, true); - } else if (!selected || temp != null) { - cubit.toolCubit.resetSelection(); - cubit.changeTool( - bloc, - index: i, - handler: handler, - context: context, - ); - } else { - cubit.toolCubit.changeSelection(tool, true); - } - }, - ), - ); - }); + ); + }, + editable: + cubit.saveCubit.state.embedding?.editable != + false, + ); }, ), ); diff --git a/app/lib/views/navigator/areas.dart b/app/lib/views/navigator/areas.dart index 41580b9a0463..6c3e87f1b8f1 100644 --- a/app/lib/views/navigator/areas.dart +++ b/app/lib/views/navigator/areas.dart @@ -268,19 +268,38 @@ class _AreasViewState extends State { bool enableButton(int dx, int dy) { if (current == null) return false; return viewState.areaNavigatorCreate || - editorController.getRelativeArea(current, dx, dy) != null; + editorController.transformCubit.getRelativeArea( + docState: state, + viewCubit: editorController.viewCubit, + currentArea: current, + dx: dx, + dy: dy, + ) != + null; } bool selectedButton(int dx, int dy) { if (current == null) return false; - return editorController.getRelativeArea(current, dx, dy, true) != + return editorController.transformCubit.getRelativeArea( + docState: state, + viewCubit: editorController.viewCubit, + currentArea: current, + dx: dx, + dy: dy, + exact: true, + ) != null; } Future navigateToRelativeArea(int dx, int dy) async { - await editorController.navigateToRelativeArea( - dx, - dy, + await editorController.transformCubit.navigateToRelativeArea( + bloc: context.read(), + settingsCubit: editorController.settingsCubit, + rendererCubit: editorController.rendererCubit, + inputCubit: editorController.inputCubit, + viewCubit: editorController.viewCubit, + dx: dx, + dy: dy, createAreaName: () => createAreaName( context, state.page, diff --git a/app/lib/views/navigator/components.dart b/app/lib/views/navigator/components.dart index bccdaff3845d..c375a309b006 100644 --- a/app/lib/views/navigator/components.dart +++ b/app/lib/views/navigator/components.dart @@ -119,8 +119,10 @@ class _ComponentsViewState extends State { key: ValueKey((e.namespace, e.key)), onTap: () => context .read() + .toolCubit .changeTemporaryHandler( context, + context.read(), StampTool(component: named), temporaryState: TemporaryState.removeAfterRelease, diff --git a/app/lib/views/toolbar/polygon.dart b/app/lib/views/toolbar/polygon.dart index 58383ebe77c6..c5cacf2784c4 100644 --- a/app/lib/views/toolbar/polygon.dart +++ b/app/lib/views/toolbar/polygon.dart @@ -37,8 +37,9 @@ class PolygonToolbarView extends StatelessWidget ), ), onEyeDropper: (context) { - bloc.editorController.changeTemporaryHandler( + bloc.editorController.toolCubit.changeTemporaryHandler( context, + bloc.editorController, EyeDropperTool(), bloc: bloc, temporaryState: TemporaryState.removeAfterRelease, diff --git a/app/lib/views/view.dart b/app/lib/views/view.dart index 1ff60b656dcd..e0fef515b352 100644 --- a/app/lib/views/view.dart +++ b/app/lib/views/view.dart @@ -192,7 +192,7 @@ class _MainViewViewportState extends State ) { final handler = getHandler(); final eventContext = getEventContext(); - cubit.updateLastPosition(event.localPosition); + cubit.inputCubit.updateLastPosition(event.localPosition); handler.onLongPressDown( LongPressDownDetails( globalPosition: event.position, @@ -328,7 +328,7 @@ class _MainViewViewportState extends State return; } } - cubit.updateLastPosition(event.localPosition); + cubit.inputCubit.updateLastPosition(event.localPosition); final ruler = _ruler; if (ruler != null && event.kind != PointerDeviceKind.touch) { ruler.transformWithPointerMove(getEventContext(), event); @@ -341,8 +341,13 @@ class _MainViewViewportState extends State } if (event.pointer == inputState.pointers.first) { final transform = context.read().state; - cubit.move( + cubit.transformCubit.moveConstrained( -event.delta / transform.size, + settingsCubit: cubit.settingsCubit, + rendererCubit: cubit.rendererCubit, + inputCubit: cubit.inputCubit, + viewCubit: cubit.viewCubit, + bloc: context.read(), currentArea: state.currentArea, ); if (!context.read().state.hasFlag('smoothNavigation')) { @@ -362,7 +367,7 @@ class _MainViewViewportState extends State _HandlerGetter getHandler, _EventContextGetter getEventContext, ) async { - cubit.updateLastPosition(event.localPosition); + cubit.inputCubit.updateLastPosition(event.localPosition); final wasRulerInteraction = _ruler != null; _resetRulerInteraction(); if (!wasRulerInteraction && (_isScalingDisabled ?? true)) { @@ -502,7 +507,7 @@ class _MainViewViewportState extends State if (nextPointerMapping == null || nextPointerMapping.getCategory() == InputMappingCategory.activeTool) { - cubit.resetDownHandler(bloc); + cubit.toolCubit.resetDownHandler(bloc, cubit.rendererCubit); return; } if (nextPointerMapping.getCategory() == @@ -511,8 +516,9 @@ class _MainViewViewportState extends State } else { final int? index = nextPointerMapping.getToolPositionIndex(); if (index != null) { - await cubit.changeTemporaryHandlerIndex( + await cubit.toolCubit.changeTemporaryHandlerIndex( context, + cubit, index, temporaryState: TemporaryState.removeAfterClick, ); @@ -531,7 +537,10 @@ class _MainViewViewportState extends State Handler getHandler() { if (state is DocumentPresentationState) return state.handler; - return cubit.getHandler(); + return cubit.toolCubit.getHandler( + editable: + cubit.saveCubit.state.embedding?.editable != false, + ); } return BlocBuilder( @@ -592,7 +601,10 @@ class _MainViewViewportState extends State getEventContext(), ); cubit.inputCubit.removeButtons(); - cubit.resetReleaseHandler(bloc); + cubit.toolCubit.resetReleaseHandler( + bloc, + cubit.rendererCubit, + ); }, onTapDown: (details) => getHandler().onTapDown( @@ -625,10 +637,17 @@ class _MainViewViewportState extends State final settings = context .read() .state; - if (cubit - .fetchHandler< - SelectHandler - >() == + if (cubit.toolCubit.fetchHandler< + SelectHandler + >( + editable: + cubit + .saveCubit + .state + .embedding + ?.editable != + false, + ) == null && !settings.inputGestures) { return; @@ -640,16 +659,25 @@ class _MainViewViewportState extends State .state .gestureSensitivity; if (details.scale == 1) { - cubit.move( + cubit.transformCubit.moveConstrained( -details.focalPointDelta / sensitivity / cubit.transformCubit.state.size, + settingsCubit: cubit.settingsCubit, + rendererCubit: cubit.rendererCubit, + inputCubit: cubit.inputCubit, + viewCubit: cubit.viewCubit, + bloc: bloc, currentArea: state.currentArea, ); } else { - cubit.zoom( + cubit.transformCubit.zoomConstrained( current / sensitivity + 1, - point, + cursor: point, + settingsCubit: cubit.settingsCubit, + rendererCubit: cubit.rendererCubit, + inputCubit: cubit.inputCubit, + viewCubit: cubit.viewCubit, ); } size = details.scale; @@ -681,11 +709,17 @@ class _MainViewViewportState extends State .state; final sensitivity = settings.gestureSensitivity; - cubit.slide( + cubit.rendererCubit + .cancelDelayedBake(); + cubit.transformCubit.slideConstrained( details.velocity.pixelsPerSecond / sensitivity / cubit.transformCubit.state.size, details.scaleVelocity, + settingsCubit: cubit.settingsCubit, + rendererCubit: cubit.rendererCubit, + inputCubit: cubit.inputCubit, + viewCubit: cubit.viewCubit, currentArea: state.currentArea, ); if (!settings.hasFlag( @@ -697,7 +731,10 @@ class _MainViewViewportState extends State _resetRulerInteraction(); cubit.inputCubit.removeButtons(); if (_isScalingDisabled ?? true) { - cubit.resetReleaseHandler(bloc); + cubit.toolCubit.resetReleaseHandler( + bloc, + cubit.rendererCubit, + ); } }, onScaleStart: (details) { @@ -706,7 +743,15 @@ class _MainViewViewportState extends State _ruler = RulerHandler.getInteractiveRuler( toolState, - cubit.getHandler(), + cubit.toolCubit.getHandler( + editable: + cubit + .saveCubit + .state + .embedding + ?.editable != + false, + ), details.localFocalPoint, constraints.biggest, ); @@ -717,17 +762,35 @@ class _MainViewViewportState extends State ); } else if (_isScalingDisabled != false) { - _isScalingDisabled = cubit - .getHandler() + _isScalingDisabled = cubit.toolCubit + .getHandler( + editable: + cubit + .saveCubit + .state + .embedding + ?.editable != + false, + ) .onScaleStart( details, getEventContext(), ); } else { - cubit.getHandler().onScaleStartAbort( - details, - getEventContext(), - ); + cubit.toolCubit + .getHandler( + editable: + cubit + .saveCubit + .state + .embedding + ?.editable != + false, + ) + .onScaleStartAbort( + details, + getEventContext(), + ); } point = details.localFocalPoint; size = 1; @@ -774,26 +837,52 @@ class _MainViewViewportState extends State _MouseState.scale) { // Calculate the new scale using dx and dy scale = -(dx + dy / 2) / 100 + 1; - cubit.zoom( - scale, - pointerSignal.localPosition, - ); + cubit.transformCubit + .zoomConstrained( + scale, + cursor: pointerSignal + .localPosition, + settingsCubit: + cubit.settingsCubit, + rendererCubit: + cubit.rendererCubit, + inputCubit: + cubit.inputCubit, + viewCubit: cubit.viewCubit, + ); } else { - cubit - ..move( - (_mouseState == - _MouseState - .inverse - ? Offset(dy, dx) - : Offset(dx, dy)) / - transform.size, - currentArea: - state.currentArea, - ) - ..zoom( - scale, - pointerSignal.localPosition, - ); + cubit.transformCubit + .moveConstrained( + (_mouseState == + _MouseState + .inverse + ? Offset(dy, dx) + : Offset(dx, dy)) / + transform.size, + settingsCubit: + cubit.settingsCubit, + rendererCubit: + cubit.rendererCubit, + inputCubit: + cubit.inputCubit, + viewCubit: cubit.viewCubit, + bloc: bloc, + currentArea: + state.currentArea, + ); + cubit.transformCubit + .zoomConstrained( + scale, + cursor: pointerSignal + .localPosition, + settingsCubit: + cubit.settingsCubit, + rendererCubit: + cubit.rendererCubit, + inputCubit: + cubit.inputCubit, + viewCubit: cubit.viewCubit, + ); } if (!settings.hasFlag( 'smoothNavigation', @@ -822,7 +911,7 @@ class _MainViewViewportState extends State ), behavior: HitTestBehavior.translucent, onPointerHover: (event) { - cubit.updateLastPosition( + cubit.inputCubit.updateLastPosition( event.localPosition, ); getHandler().onPointerHover( diff --git a/app/lib/views/zoom.dart b/app/lib/views/zoom.dart index 53fba72b117f..7e4a98bad116 100644 --- a/app/lib/views/zoom.dart +++ b/app/lib/views/zoom.dart @@ -74,9 +74,17 @@ class _ZoomViewState extends State with TickerProviderStateMixin { } final size = rendererState.cameraViewport.toRealSize(); final center = Offset(size.width / 2, size.height / 2); - editorController.size(value, center, true); + editorController.transformCubit.sizeConstrained( + value, + cursor: center, + force: true, + settingsCubit: editorController.settingsCubit, + rendererCubit: editorController.rendererCubit, + inputCubit: editorController.inputCubit, + viewCubit: editorController.viewCubit, + ); if (bake) { - editorController.bake(documentState); + editorController.rendererCubit.bake(editorController, documentState); } final settings = context.read().state; diff --git a/app/lib/widgets/search.dart b/app/lib/widgets/search.dart index 6571b4031e84..70e02010fa34 100644 --- a/app/lib/widgets/search.dart +++ b/app/lib/widgets/search.dart @@ -1,5 +1,4 @@ import 'package:butterfly/bloc/document_bloc.dart'; -import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/helpers/point.dart'; import 'package:butterfly/visualizer/element.dart'; import 'package:butterfly/visualizer/tool.dart'; @@ -116,10 +115,15 @@ class SearchButton extends StatelessWidget { if (position != null) { bloc.transformCubit.teleport(position.toOffset()); } - cubit.bake(state); + cubit.rendererCubit.bake(cubit, state); if (result is ToolResult) { cubit.toolCubit.resetSelection(); - cubit.changeTool(bloc, index: result.index, context: context); + cubit.toolCubit.changeTool( + cubit, + bloc, + index: result.index, + context: context, + ); } Navigator.pop(context); }, diff --git a/app/test/bloc/document_bloc_test.dart b/app/test/bloc/document_bloc_test.dart index abe4781ffc09..7385ba6da2e7 100644 --- a/app/test/bloc/document_bloc_test.dart +++ b/app/test/bloc/document_bloc_test.dart @@ -501,8 +501,14 @@ void main() { pageName, ); - await editorController.loadElements(bloc.state); - await editorController.loadElements(bloc.state); + await editorController.rendererCubit.loadElements( + editorController, + bloc.state, + ); + await editorController.rendererCubit.loadElements( + editorController, + bloc.state, + ); expect(renderer.onVisibleCalls, 2); }); @@ -680,7 +686,8 @@ void main() { page, pageName, ); - await editorController.bake( + await editorController.rendererCubit.bake( + editorController, bloc.state as DocumentLoadSuccess, viewportSize: const Size(100, 100), pixelRatio: 1, @@ -761,7 +768,8 @@ void main() { pageName, ); - await editorController.bake( + await editorController.rendererCubit.bake( + editorController, bloc.state as DocumentLoadSuccess, viewportSize: const Size(100, 100), pixelRatio: 1, @@ -821,7 +829,8 @@ void main() { pageName, ); - await editorController.bake( + await editorController.rendererCubit.bake( + editorController, bloc.state as DocumentLoadSuccess, viewportSize: const Size(100, 100), pixelRatio: 1, @@ -833,7 +842,8 @@ void main() { isEmpty, ); - await editorController.bake( + await editorController.rendererCubit.bake( + editorController, bloc.state as DocumentLoadSuccess, viewportSize: const Size(100, 100), pixelRatio: 2, @@ -892,7 +902,8 @@ void main() { pageName, ); - await editorController.bake( + await editorController.rendererCubit.bake( + editorController, bloc.state as DocumentLoadSuccess, viewportSize: const Size(401, 303), pixelRatio: 1, @@ -956,7 +967,8 @@ void main() { pageName, ); - await editorController.bake( + await editorController.rendererCubit.bake( + editorController, bloc.state as DocumentLoadSuccess, viewportSize: const Size(100, 100), pixelRatio: 1, @@ -964,14 +976,16 @@ void main() { ); renderer.onVisibleCalls = 0; renderer.onHiddenCalls = 0; - await editorController.renderImage( + await editorController.rendererCubit.renderImage( + editorController, (bloc.state as DocumentLoadSuccess).data, page, (bloc.state as DocumentLoadSuccess).info, const ImageExportOptions(width: 100, height: 100), docState: bloc.state as DocumentLoadSuccess, ); - await editorController.bake( + await editorController.rendererCubit.bake( + editorController, bloc.state as DocumentLoadSuccess, viewportSize: const Size(100, 100), pixelRatio: 1, @@ -1031,7 +1045,8 @@ void main() { pageName, ); - final image = await editorController.renderImage( + final image = await editorController.rendererCubit.renderImage( + editorController, data, page, (bloc.state as DocumentLoadSuccess).info, From ec3152a52bdb43c27b863b32a146a5570c2a520e Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 30 Jun 2026 09:08:29 +0200 Subject: [PATCH 035/117] Improve parameters, split cubits in different files --- app/lib/actions/zoom.dart | 5 +- app/lib/cubits/document_save.dart | 149 ++ app/lib/cubits/editor_controller.dart | 146 +- app/lib/cubits/editor_input.dart | 129 ++ app/lib/cubits/editor_renderer.dart | 1049 +++++++++ app/lib/cubits/editor_runtime.dart | 2339 +------------------- app/lib/cubits/editor_runtime.freezed.dart | 100 +- app/lib/cubits/editor_tool.dart | 1045 +++++++++ app/lib/cubits/editor_view.dart | 82 + app/lib/cubits/transform.dart | 133 +- app/lib/cubits/transform.freezed.dart | 40 +- app/lib/helpers/point.dart | 86 +- app/lib/selections/document.dart | 5 +- app/lib/views/navigator/areas.dart | 5 +- app/lib/views/view.dart | 44 +- app/lib/views/zoom.dart | 5 +- 16 files changed, 2702 insertions(+), 2660 deletions(-) create mode 100644 app/lib/cubits/document_save.dart create mode 100644 app/lib/cubits/editor_input.dart create mode 100644 app/lib/cubits/editor_renderer.dart create mode 100644 app/lib/cubits/editor_tool.dart create mode 100644 app/lib/cubits/editor_view.dart diff --git a/app/lib/actions/zoom.dart b/app/lib/actions/zoom.dart index 4c9994b06dd7..0438281f4115 100644 --- a/app/lib/actions/zoom.dart +++ b/app/lib/actions/zoom.dart @@ -39,10 +39,7 @@ class ZoomAction extends Action { cubit.transformCubit.sizeConstrained( transformCubit.state.size + (intent.reverse ? -0.1 : 0.1), cursor: center, - settingsCubit: cubit.settingsCubit, - rendererCubit: cubit.rendererCubit, - inputCubit: cubit.inputCubit, - viewCubit: cubit.viewCubit, + runtime: cubit, ); } } diff --git a/app/lib/cubits/document_save.dart b/app/lib/cubits/document_save.dart new file mode 100644 index 000000000000..cbd921133ac6 --- /dev/null +++ b/app/lib/cubits/document_save.dart @@ -0,0 +1,149 @@ +part of 'editor_runtime.dart'; + +@freezed +sealed class DocumentSaveState with _$DocumentSaveState { + const DocumentSaveState._(); + + const factory DocumentSaveState({ + @Default(false) bool isSaveDelayed, + @Default(AssetLocation(path: '')) AssetLocation location, + Embedding? embedding, + @Default(SaveState.saved) SaveState saved, + @Default(false) bool isCreating, + }) = _DocumentSaveState; + + bool get absolute => saved == SaveState.absoluteRead; +} + +class DocumentSaveCubit extends Cubit { + DocumentSaveCubit( + this.settingsCubit, [ + super.initial = const DocumentSaveState(), + ]); + + final SettingsCubit settingsCubit; + + final savingLock = Lock(); + + void replace(DocumentSaveState state) => emit(state); + + void setSaveState({ + AssetLocation? location, + SaveState? saved, + bool absolute = false, + bool? isCreating, + bool keepRead = false, + }) => emit( + state.copyWith( + location: location ?? state.location, + isCreating: isCreating ?? state.isCreating, + saved: (absolute || (keepRead && state.absolute)) + ? SaveState.absoluteRead + : saved ?? state.saved, + ), + ); + + void setDelayed(bool delayed) => emit(state.copyWith(isSaveDelayed: delayed)); + + ExternalStorage? getRemoteStorage() => + settingsCubit.getRemote(state.location.remote); + + bool hasAutosave(NetworkingService networkingService) => + settingsCubit.state.autosave && + (networkingService.isActive || + !(state.embedding?.save ?? true) || + (!kIsWeb && + !state.absolute && + (state.location.isEmpty || + (state.location.fileType?.isNote() ?? false)) && + (state.location.remote.isEmpty || + (settingsCubit + .getRemote(state.location.remote) + ?.hasDocumentCached(state.location.path) ?? + false)))); + + Future save( + DocumentBloc bloc, + NetworkingService networkingService, { + AssetLocation? location, + bool force = false, + bool isAutosave = false, + }) async { + final absolute = state.absolute; + if (location == null && + !force && + (state.saved == SaveState.saved || + state.saved == SaveState.absoluteRead)) { + return state.location; + } + if (networkingService.isClient) { + return AssetLocation.empty; + } + if (state.isSaveDelayed && isAutosave) { + return state.location; + } + final storage = getRemoteStorage(); + final fileSystem = bloc.state.fileSystem.buildDocumentSystem(storage); + final isDelayed = settingsCubit.state.delayedAutosave; + if (isDelayed && isAutosave) { + final seconds = max(0, settingsCubit.state.autosaveDelaySeconds); + setDelayed(true); + await Future.delayed(Duration(seconds: seconds)); + if (!state.isSaveDelayed) { + return state.location; + } + } + return savingLock.synchronized(() async { + if (location == null && + !force && + (state.saved == SaveState.saved || + state.saved == SaveState.absoluteRead)) { + return state.location; + } + var current = location ?? state.location; + if (isClosed) { + return current; + } + setSaveState(saved: SaveState.saving, location: current); + setDelayed(false); + final blocState = bloc.state; + final currentData = await blocState.saveData(); + if (isClosed) { + return current; + } + if (currentData == null || state.embedding != null) { + setSaveState(saved: SaveState.saved); + return AssetLocation.empty; + } + if (absolute || !(current.fileType?.isNote() ?? false)) { + final file = await compute(_toFile, (currentData, false)); + final document = await fileSystem.createFileWithName( + name: currentData.name, + suffix: '.bfly', + directory: absolute + ? null + : current.fileExtension.isEmpty + ? state.location.path + : state.location.parent, + file, + ); + current = document.location; + } else { + final file = await compute(_toFile, ( + currentData, + current.fileType == AssetFileType.textNote, + )); + await fileSystem.updateFile(current.path, file); + } + settingsCubit.addRecentHistory(current); + if (isClosed) { + return current; + } + setSaveState( + saved: state.saved == SaveState.saving ? SaveState.saved : state.saved, + location: current, + ); + return current; + }); + } +} diff --git a/app/lib/cubits/editor_controller.dart b/app/lib/cubits/editor_controller.dart index d57800c1e380..b1c23d64de05 100644 --- a/app/lib/cubits/editor_controller.dart +++ b/app/lib/cubits/editor_controller.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:butterfly/bloc/document_bloc.dart'; import 'package:butterfly/cubits/editor_session.dart'; import 'package:butterfly/cubits/editor_runtime.dart'; @@ -13,8 +11,6 @@ import 'package:butterfly/views/navigator/view.dart'; import 'package:butterfly/visualizer/tool.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:material_leap/material_leap.dart'; import 'package:networker/networker.dart'; import '../embed/embedding.dart'; @@ -27,6 +23,7 @@ export 'editor_runtime.dart' DocumentSaveState, EditorInputCubit, EditorInputState, + EditorRuntimeContext, EditorViewCubit, EditorViewState, HideState, @@ -38,20 +35,20 @@ export 'editor_runtime.dart' ToolCubit, ToolRuntimeState; -class EditorController { +class EditorController implements EditorRuntimeContext { + @override final SettingsCubit settingsCubit; final TransformCubit transformCubit; final NetworkingService networkingService; final EditorSessionCubit? editorSessionCubit; - late final RendererCubit rendererCubit; - late final ToolCubit toolCubit; - late final EditorInputCubit inputCubit; - late final DocumentSaveCubit saveCubit; - late final EditorViewCubit viewCubit; - StreamSubscription? _rendererSubscription; - StreamSubscription? _toolSubscription; - StreamSubscription? _inputSubscription; - StreamSubscription? _viewSubscription; + @override + final RendererCubit rendererCubit; + final ToolCubit toolCubit; + @override + final EditorInputCubit inputCubit; + final DocumentSaveCubit saveCubit; + @override + final EditorViewCubit viewCubit; EditorController( this.settingsCubit, @@ -97,27 +94,14 @@ class EditorController { editorSessionCubit?.state.areaNavigatorAsk ?? false, ), ) { - _previousRendererState = rendererCubit.state; - _previousToolState = toolCubit.state; - _previousInputState = inputCubit.state; - _previousViewState = viewCubit.state; - _transformSubscription = transformCubit.stream.listen(_onTransformChanged); - _rendererSubscription = rendererCubit.stream.listen(_onRendererChanged); - _toolSubscription = toolCubit.stream.listen(_onToolChanged); - _inputSubscription = inputCubit.stream.listen(_onInputChanged); - _viewSubscription = viewCubit.stream.listen(_onViewChanged); + rendererCubit.bindController(this); + toolCubit.bindController(this); + inputCubit.bindToolCubit(toolCubit); + viewCubit.bindToolCubit(toolCubit); } - StreamSubscription? _transformSubscription; - Timer? _transformDebounceTimer; - Timer? _networkingDebounceTimer; - RendererRuntimeState _previousRendererState = const RendererRuntimeState(); - ToolRuntimeState? _previousToolState; - EditorInputState _previousInputState = const EditorInputState(); - EditorViewState? _previousViewState; WeakReference? _documentBloc; var _closed = false; - var _isClosing = false; bool get isClosed => _closed; @@ -135,14 +119,6 @@ class EditorController { Future reload(DocumentBloc bloc, [DocumentLoaded? blocState]) => reloadRuntime(bloc, blocState); - void _onTransformChanged(CameraTransform transform) { - // Debounce transform changes to avoid excessive updates during pan/zoom - _transformDebounceTimer?.cancel(); - _transformDebounceTimer = Timer(const Duration(milliseconds: 16), () { - rendererCubit.updateVisibleElements(this, activeDocumentBloc); - }); - } - void init(DocumentBloc bloc) { _documentBloc = WeakReference(bloc); final blocState = bloc.state; @@ -153,83 +129,6 @@ class EditorController { networkingService.setup(bloc); } - void _onToolChanged(ToolRuntimeState next) { - if (_isClosing) { - return; - } - final current = _previousToolState; - _previousToolState = next; - if (current == null) return; - - if (next.foregrounds != current.foregrounds || - next.temporaryForegrounds != current.temporaryForegrounds) { - _networkingDebounceTimer?.cancel(); - _networkingDebounceTimer = Timer(const Duration(milliseconds: 50), () { - if (!isClosed) _sendNetworkingState(); - }); - } - } - - void _onInputChanged(EditorInputState next) { - if (_isClosing) return; - final current = _previousInputState; - _previousInputState = next; - if (next.lastPosition != current.lastPosition) { - _networkingDebounceTimer?.cancel(); - _networkingDebounceTimer = Timer(const Duration(milliseconds: 50), () { - if (!isClosed) _sendNetworkingState(); - }); - } - } - - void _onViewChanged(EditorViewState next) { - if (_isClosing) return; - final current = _previousViewState; - _previousViewState = next; - if (current != null && next.userName != current.userName) { - _networkingDebounceTimer?.cancel(); - _networkingDebounceTimer = Timer(const Duration(milliseconds: 50), () { - if (!isClosed) _sendNetworkingState(); - }); - } - } - - void _onRendererChanged(RendererRuntimeState next) { - if (_isClosing) return; - final current = _previousRendererState; - _previousRendererState = next; - final currentViewport = current.cameraViewport; - final newViewport = next.cameraViewport; - - if (!identical(currentViewport, newViewport) && - currentViewport != newViewport) { - toolCubit.state.handler.onViewportUpdated(currentViewport, newViewport); - toolCubit.state.temporaryHandler?.onViewportUpdated( - currentViewport, - newViewport, - ); - } - - currentViewport.disposeImages(except: newViewport); - } - - void _sendNetworkingState({ - List>? foregrounds, - Offset? cursor, - }) { - cursor ??= inputCubit.state.lastPosition ?? Offset.zero; - networkingService.sendUser( - NetworkingUser( - cursor: transformCubit.state.localToGlobal(cursor).toPoint(), - foreground: (foregrounds ?? toolCubit.state.getAllForegrounds(false)) - .map((e) => e.element) - .whereType() - .toList(), - name: networkingService.userName, - ), - ); - } - Future updateNetworkingState( DocumentBloc bloc, [ Map? current, @@ -295,7 +194,6 @@ class EditorController { Future close() async { if (_closed) return; - _isClosing = true; _closed = true; final bloc = activeDocumentBloc; if (bloc != null) { @@ -303,20 +201,6 @@ class EditorController { } _documentBloc = null; await rendererCubit.disposeRuntime(); - await _transformSubscription?.cancel(); - _transformSubscription = null; - await _rendererSubscription?.cancel(); - _rendererSubscription = null; - await _toolSubscription?.cancel(); - _toolSubscription = null; - await _inputSubscription?.cancel(); - _inputSubscription = null; - await _viewSubscription?.cancel(); - _viewSubscription = null; - _transformDebounceTimer?.cancel(); - _transformDebounceTimer = null; - _networkingDebounceTimer?.cancel(); - _networkingDebounceTimer = null; await rendererCubit.close(); await toolCubit.close(); await inputCubit.close(); diff --git a/app/lib/cubits/editor_input.dart b/app/lib/cubits/editor_input.dart new file mode 100644 index 000000000000..d529877d4a02 --- /dev/null +++ b/app/lib/cubits/editor_input.dart @@ -0,0 +1,129 @@ +part of 'editor_runtime.dart'; + +@freezed +sealed class EditorInputState with _$EditorInputState { + const factory EditorInputState({ + Offset? lastPosition, + @Default([]) List pointers, + int? buttons, + @Default(false) bool penDetected, + @Default(false) bool sessionPenOnlyInput, + @Default(HideState.visible) HideState hideUi, + }) = _EditorInputState; +} + +class EditorInputCubit extends Cubit { + EditorInputCubit( + this.settingsCubit, [ + super.initial = const EditorInputState(), + ]); + + final SettingsCubit settingsCubit; + ToolCubit? _toolCubit; + + void bindToolCubit(ToolCubit toolCubit) { + _toolCubit = toolCubit; + } + + @override + void onChange(Change change) { + super.onChange(change); + if (change.nextState.lastPosition != change.currentState.lastPosition) { + _toolCubit?.scheduleNetworkingState(); + } + } + + void replace(EditorInputState state) => emit(state); + + void setPenDetected(bool detected, {bool enableSessionPenOnly = false}) { + if (state.penDetected == detected && + (!enableSessionPenOnly || state.sessionPenOnlyInput)) { + return; + } + emit( + state.copyWith( + penDetected: detected, + sessionPenOnlyInput: enableSessionPenOnly + ? true + : state.sessionPenOnlyInput, + ), + ); + } + + bool get effectivePenOnlyInput { + final setting = settingsCubit.state.penOnlyInput; + if (setting != null) return setting; + return state.sessionPenOnlyInput; + } + + bool get moveEnabled => + (settingsCubit.state.inputGestures && state.pointers.length > 1) && + settingsCubit.state.moveOnGesture; + + void detectPen(bool detected) { + if (state.penDetected == detected) return; + setPenDetected( + detected, + enableSessionPenOnly: + detected && + settingsCubit.state.penOnlyInput == null && + !state.sessionPenOnlyInput, + ); + } + + void setSessionPenOnlyInput(bool value) { + if (state.sessionPenOnlyInput != value) { + emit(state.copyWith(sessionPenOnlyInput: value)); + } + } + + void updateLastPosition(Offset position) { + final lastPos = state.lastPosition; + if (lastPos != null) { + final dx = (position.dx - lastPos.dx).abs(); + final dy = (position.dy - lastPos.dy).abs(); + if (dx < 1 && dy < 1) return; + } + emit(state.copyWith(lastPosition: position)); + } + + void addPointer(int pointer) { + if (!state.pointers.contains(pointer)) { + emit(state.copyWith(pointers: [...state.pointers, pointer])); + } + } + + void removePointer(int pointer) { + if (state.pointers.contains(pointer)) { + emit( + state.copyWith( + pointers: state.pointers.where((p) => p != pointer).toList(), + ), + ); + } + } + + void setButtons(int buttons) => emit(state.copyWith(buttons: buttons)); + + void removeButtons() => emit(state.copyWith(buttons: null)); + + void resetInputState() => emit(state.copyWith(buttons: null, pointers: [])); + + void toggleKeyboardHideUI() => emit( + state.copyWith( + hideUi: state.hideUi == HideState.visible + ? HideState.keyboard + : HideState.visible, + ), + ); + + void enterTouchHideUI() => emit(state.copyWith(hideUi: HideState.touch)); + + void exitHideUI() => emit(state.copyWith(hideUi: HideState.visible)); + + @override + Future close() { + _toolCubit = null; + return super.close(); + } +} diff --git a/app/lib/cubits/editor_renderer.dart b/app/lib/cubits/editor_renderer.dart new file mode 100644 index 000000000000..40fa97b533a5 --- /dev/null +++ b/app/lib/cubits/editor_renderer.dart @@ -0,0 +1,1049 @@ +part of 'editor_runtime.dart'; + +@Freezed(equal: false) +sealed class RendererRuntimeState with _$RendererRuntimeState { + const RendererRuntimeState._(); + + const factory RendererRuntimeState({ + @Default(CameraViewport.unbaked()) CameraViewport cameraViewport, + @Default({}) Map rendererStates, + @Default({}) Map? temporaryRendererStates, + }) = _RendererRuntimeState; + + Map get allRendererStates => { + ...rendererStates, + ...?temporaryRendererStates, + }; +} + +class RendererCubit extends Cubit { + RendererCubit( + this.settingsCubit, [ + super.initial = const RendererRuntimeState(), + ]); + + final SettingsCubit settingsCubit; + + final initializedElements = >{}; + final bakeLock = Lock(); + final delayedBakeRunner = CoalescedAsyncRunner( + delay: const Duration(milliseconds: 100), + ); + EditorController? _controller; + StreamSubscription? _transformSubscription; + Timer? _transformDebounceTimer; + + void bindController(EditorController controller) { + _controller = controller; + _transformSubscription?.cancel(); + _transformSubscription = controller.transformCubit.stream.listen((_) { + _transformDebounceTimer?.cancel(); + _transformDebounceTimer = Timer(const Duration(milliseconds: 16), () { + if (!controller.isClosed) { + updateVisibleElements(controller, controller.activeDocumentBloc); + } + }); + }); + } + + @override + void onChange(Change change) { + super.onChange(change); + final controller = _controller; + if (controller == null || controller.isClosed) return; + final currentViewport = change.currentState.cameraViewport; + final newViewport = change.nextState.cameraViewport; + + if (!identical(currentViewport, newViewport) && + currentViewport != newViewport) { + controller.toolCubit.state.handler.onViewportUpdated( + currentViewport, + newViewport, + ); + controller.toolCubit.state.temporaryHandler?.onViewportUpdated( + currentViewport, + newViewport, + ); + } + + currentViewport.disposeImages(except: newViewport); + } + + void cancelDelayedBake() { + delayedBakeRunner.cancel(); + } + + void replace(RendererRuntimeState state) => emit(state); + + void setViewport(CameraViewport cameraViewport) => + emit(state.copyWith(cameraViewport: cameraViewport)); + + void setRendererStates({ + Map? rendererStates, + Map? temporaryRendererStates, + }) => emit( + state.copyWith( + rendererStates: rendererStates ?? state.rendererStates, + temporaryRendererStates: + temporaryRendererStates ?? state.temporaryRendererStates, + ), + ); + + List> get renderers => + List>.from(state.cameraViewport.bakedElements) + ..addAll(state.cameraViewport.unbakedElements); + + Renderer? getRenderer(PadElement element) => + renderers.firstWhereOrNull((renderer) => renderer.element == element); + + bool sameRendererList( + List> a, + List> b, + ) { + if (identical(a, b)) return true; + if (a.length != b.length) return false; + for (var i = 0; i < a.length; i++) { + if (!identical(a[i], b[i])) return false; + } + return true; + } + + void invalidateRenderers(Iterable> renderers) { + initializedElements.removeAll(renderers); + } + + Rect getViewportRect(TransformCubit transformCubit, {Size? viewportSize}) { + var size = viewportSize ?? state.cameraViewport.toSize(); + final transform = transformCubit.state; + final resolution = settingsCubit.state.renderResolution; + final friction = transform.friction; + final realWidth = size.width / transform.size; + final realHeight = size.height / transform.size; + Rect rect = Rect.fromLTWH( + transform.position.dx, + transform.position.dy, + realWidth, + realHeight, + ); + if (friction != null) { + final beginPosition = transform.position - friction.beginOffset; + final topLeft = Offset( + min(transform.position.dx, beginPosition.dx), + min(transform.position.dy, beginPosition.dy), + ); + final frictionSize = Size( + realWidth + (friction.beginOffset.dx * transform.size).abs(), + realHeight + (friction.beginOffset.dy * transform.size).abs(), + ); + rect = topLeft & frictionSize; + } + return _snapViewportRect(rect, size, transform, resolution); + } + + Rect _snapViewportRect( + Rect rect, + Size size, + CameraTransform transform, + RenderResolution resolution, + ) { + final screenRect = Rect.fromPoints( + transform.globalToLocal(rect.topLeft), + transform.globalToLocal(rect.bottomRight), + ); + final snappedRect = _expandScreenRect( + Rect.fromLTRB( + screenRect.left.floorToDouble(), + screenRect.top.floorToDouble(), + screenRect.right.ceilToDouble(), + screenRect.bottom.ceilToDouble(), + ), + Size( + (size.width * resolution.multiplier).ceilToDouble(), + (size.height * resolution.multiplier).ceilToDouble(), + ), + ); + return Rect.fromPoints( + transform.localToGlobal(snappedRect.topLeft), + transform.localToGlobal(snappedRect.bottomRight), + ); + } + + Rect _expandScreenRect(Rect rect, Size minimumSize) { + final dx = max(0.0, minimumSize.width - rect.width); + final dy = max(0.0, minimumSize.height - rect.height); + return Rect.fromLTRB( + rect.left - (dx / 2).floorToDouble(), + rect.top - (dy / 2).floorToDouble(), + rect.right + (dx / 2).ceilToDouble(), + rect.bottom + (dy / 2).ceilToDouble(), + ); + } + + bool rectContains(Rect outer, Rect inner) { + const tolerance = precisionErrorTolerance; + return outer.left <= inner.left + tolerance && + outer.top <= inner.top + tolerance && + outer.right >= inner.right - tolerance && + outer.bottom >= inner.bottom - tolerance; + } + + Rect getPageRect({Set? invisibleLayers}) { + Rect? rect; + for (final renderer in renderers) { + final rendererRect = renderer.expandedRect; + if (rendererRect == null) continue; + if (invisibleLayers?.contains(renderer.layer) ?? false) continue; + rect = rect?.expandToInclude(rendererRect) ?? rendererRect; + } + return rect ?? Rect.zero; + } + + Future updateVisibleElements( + EditorController controller, + DocumentBloc? bloc, + ) async { + if (controller.isClosed) return; + final unbaked = state.cameraViewport.unbakedElements; + final baked = state.cameraViewport.bakedElements; + + final rect = getViewportRect(controller.transformCubit); + final currentVisible = state.cameraViewport.visibleElements; + final currentVisibleUnbaked = state.cameraViewport.visibleUnbakedElements; + + final visibleUnbaked = unbaked.where((e) => e.isVisible(rect)).toList(); + final visible = >[ + ...baked.where((e) => e.isVisible(rect)), + ...visibleUnbaked, + ]; + + if (sameRendererList(visible, currentVisible) && + sameRendererList(visibleUnbaked, currentVisibleUnbaked)) { + return; + } + + final newViewport = state.cameraViewport.withUnbaked( + unbaked, + visibleElements: visible, + visibleUnbakedElements: visibleUnbaked, + ); + + final docState = bloc?.state; + if (docState is DocumentLoaded) { + await updateOnVisible(controller, newViewport, docState); + if (!controller.isClosed && bloc != null && !bloc.isClosed) { + bloc.delayedBake(); + } + } + + if (controller.isClosed) return; + + setViewport(newViewport); + } + + Future updateOnVisible( + EditorController controller, + CameraViewport newViewport, + DocumentLoaded blocState, { + CameraTransform? renderTransform, + ui.Size? targetSize, + }) async { + final newVisibleList = newViewport.visibleElements; + final nextVisibleSet = newVisibleList.toSet(); + + final newVisible = newVisibleList + .where((e) => !initializedElements.contains(e)) + .toList(); + + final newlyHidden = initializedElements + .where((e) => !nextVisibleSet.contains(e)) + .toList(); + + if (newVisible.isEmpty && newlyHidden.isEmpty) return; + + final transform = renderTransform ?? controller.transformCubit.state; + final size = targetSize ?? newViewport.toSize(); + + initializedElements.removeAll(newlyHidden); + + if (newVisible.isNotEmpty) { + talker.verbose('Updating visible elements: ${newVisible.length} new'); + final initialized = await Future.wait( + newVisible.map((element) async { + try { + await Future.sync( + () => element.onVisible(controller, blocState, transform, size), + ); + return element; + } catch (error, stackTrace) { + talker.error( + 'Failed to initialize visible renderer $element', + error, + stackTrace, + ); + } + return null; + }), + ); + initializedElements.addAll(initialized.nonNulls); + } + + if (newlyHidden.isNotEmpty) { + await Future.wait( + newlyHidden.map( + (element) async => + await element.onHidden(controller, blocState, transform, size), + ), + ); + } + } + + Future delayedBake( + EditorController controller, + DocumentLoaded blocState, { + ui.Size? viewportSize, + double? pixelRatio, + bool reset = false, + bool testTransform = false, + }) => delayedBakeRunner.schedule(() async { + final newTransform = controller.transformCubit.state; + final viewport = state.cameraViewport; + + if (testTransform && + newTransform.size == viewport.scale && + newTransform.position == viewport.toOffset()) { + return; + } + + await controller.rendererCubit.bake( + controller, + blocState, + viewportSize: viewportSize, + pixelRatio: pixelRatio, + reset: reset, + ); + }); + + Future bake( + EditorController controller, + DocumentLoaded blocState, { + Size? viewportSize, + double? pixelRatio, + bool reset = false, + bool resetAllLayers = false, + }) => bakeLock.synchronized(() async { + final rendererCubit = this; + final transformCubit = controller.transformCubit; + final settingsCubit = controller.settingsCubit; + if (controller.isClosed) return; + var cameraViewport = rendererCubit.state.cameraViewport; + final startTransform = transformCubit.state; + final startViewport = cameraViewport; + final resolution = settingsCubit.state.renderResolution; + var size = viewportSize ?? cameraViewport.toSize(); + final ratio = pixelRatio ?? cameraViewport.pixelRatio; + if (size.height <= 0 || size.width <= 0) { + return; + } + if (viewportSize == null) { + size /= resolution.multiplier; + } + var transform = transformCubit.state; + var renderers = List>.from(rendererCubit.renderers); + final recorder = ui.PictureRecorder(); + final canvas = ui.Canvas(recorder); + final rect = rendererCubit.getViewportRect( + transformCubit, + viewportSize: size, + ); + size = rect.size * transform.size; + final renderTransform = transform.improve(resolution, rect); + final document = blocState.data; + final page = blocState.page; + final info = blocState.info; + final imageWidth = (size.width * ratio).ceil(); + final imageHeight = (size.height * ratio).ceil(); + var allRendererStates = rendererCubit.state.allRendererStates; + final rendererStatesChanged = !mapEquals( + allRendererStates, + cameraViewport.rendererStates, + ); + if (!rendererStatesChanged) { + allRendererStates = cameraViewport.rendererStates; + } + final invisibleLayers = blocState.invisibleLayers; + final viewportAlreadyCoversRect = + cameraViewport.image != null && + cameraViewport.scale == transform.size && + cameraViewport.resolution == resolution && + cameraViewport.pixelRatio == ratio && + !rendererStatesChanged && + setEquals(cameraViewport.invisibleLayers, invisibleLayers) && + rendererCubit.rectContains(cameraViewport.toRect(), rect); + final viewChanged = + !viewportAlreadyCoversRect && + (cameraViewport.width != size.width.ceil() || + cameraViewport.height != size.height.ceil() || + cameraViewport.pixelRatio != ratio || + cameraViewport.resolution != resolution || + cameraViewport.x != renderTransform.position.dx || + cameraViewport.y != renderTransform.position.dy || + cameraViewport.scale != transform.size || + rendererStatesChanged || + !setEquals(cameraViewport.invisibleLayers, invisibleLayers)); + reset = reset || viewChanged; + resetAllLayers = resetAllLayers || viewChanged; + if (cameraViewport.unbakedElements.isEmpty && !reset) return; + final currentLayer = blocState.currentLayer; + List> visibleElements; + final oldVisible = cameraViewport.visibleElements; + final oldVisibleSet = oldVisible.toSet(); + talker.verbose( + 'Baking viewport (reset: $reset, viewChanged: $viewChanged, ' + 'rendererStatesChanged: $rendererStatesChanged)', + ); + + if (reset) { + visibleElements = renderers + .where((renderer) => renderer.isVisible(rect)) + .toList(); + } else { + visibleElements = List.from(oldVisible) + ..addAll( + cameraViewport.unbakedElements.where( + (renderer) => + !oldVisibleSet.contains(renderer) && renderer.isVisible(rect), + ), + ); + } + + final visibleElementsSet = visibleElements.toSet(); + + await rendererCubit.updateOnVisible( + controller, + cameraViewport.unbake(visibleElements: visibleElements), + blocState, + renderTransform: renderTransform, + targetSize: size, + ); + + canvas.scale(ratio); + + if (viewChanged && visibleElements.isNotEmpty) { + await Future.wait( + visibleElements.map( + (e) async => + await e.updateView(controller, blocState, renderTransform, size), + ), + ); + } + + // Wait one frame + await Future.delayed(const Duration(milliseconds: 1)); + + ViewPainter( + document, + page, + info, + transform: renderTransform, + cameraViewport: reset + ? cameraViewport.unbake( + rendererStates: allRendererStates, + unbakedElements: visibleElements + .where((e) => currentLayer == e.layer) + .toList(), + visibleElements: visibleElements, + ) + : cameraViewport, + renderBackground: false, + renderBaked: !reset, + renderBakedLayers: false, + invisibleLayers: invisibleLayers, + ).paint(canvas, size); + + final picture = recorder.endRecording(); + ui.Image newImage; + try { + newImage = await picture.toImage(imageWidth, imageHeight); + } finally { + picture.dispose(); + } + + var belowLayerImage = cameraViewport.belowLayerImage; + var aboveLayerImage = cameraViewport.aboveLayerImage; + + if (resetAllLayers) { + final belowLayerRecorder = ui.PictureRecorder(); + final belowLayerCanvas = ui.Canvas(belowLayerRecorder); + belowLayerCanvas.scale(ratio); + final aboveLayerRecorder = ui.PictureRecorder(); + final aboveLayerCanvas = ui.Canvas(aboveLayerRecorder); + aboveLayerCanvas.scale(ratio); + final belowLayers = {}, aboveLayers = {}; + bool above = false; + for (final layer in page.layers) { + if (layer.id == currentLayer) { + above = true; + continue; + } + final layerId = layer.id; + if (layerId == null) continue; + if (above) { + aboveLayers.add(layerId); + } else { + belowLayers.add(layerId); + } + } + + ViewPainter( + document, + page, + info, + transform: renderTransform, + cameraViewport: cameraViewport.unbake( + rendererStates: allRendererStates, + unbakedElements: visibleElements + .where((e) => e.layer != null && belowLayers.contains(e.layer)) + .toList(), + visibleElements: visibleElements, + ), + renderBackground: false, + renderBaked: false, + invisibleLayers: invisibleLayers, + ).paint(belowLayerCanvas, size); + ViewPainter( + document, + page, + info, + transform: renderTransform, + cameraViewport: cameraViewport.unbake( + rendererStates: allRendererStates, + unbakedElements: visibleElements + .where((e) => e.layer != null && aboveLayers.contains(e.layer)) + .toList(), + visibleElements: visibleElements, + ), + renderBackground: false, + renderBaked: false, + invisibleLayers: invisibleLayers, + ).paint(aboveLayerCanvas, size); + + final belowPicture = belowLayerRecorder.endRecording(); + final abovePicture = aboveLayerRecorder.endRecording(); + try { + final result = await Future.wait([ + belowPicture.toImage(imageWidth, imageHeight), + abovePicture.toImage(imageWidth, imageHeight), + ]); + belowLayerImage = result[0]; + aboveLayerImage = result[1]; + } finally { + belowPicture.dispose(); + abovePicture.dispose(); + } + } + + final bakedElementsSet = cameraViewport.bakedElements + .map((e) => e.element) + .toSet(); + final unbakedElementsSet = cameraViewport.unbakedElements + .map((e) => e.element) + .toSet(); + + final newlyUnbaked = + (reset + ? rendererCubit.renderers + : rendererCubit.state.cameraViewport.unbakedElements) + .where( + (element) => + !bakedElementsSet.contains(element.element) && + !unbakedElementsSet.contains(element.element) && + !visibleElementsSet.contains(element), + ) + .toList(); + + if (controller.isClosed) return; + + // If state changed while baking (e.g. fast move submitted a newer viewport), + // this bake output is stale and must not overwrite the latest viewport. + final currentViewport = rendererCubit.state.cameraViewport; + final currentTransform = transformCubit.state; + if (!identical(currentViewport, startViewport) || + currentTransform != startTransform) { + newImage.dispose(); + final oldBelow = startViewport.belowLayerImage; + final oldAbove = startViewport.aboveLayerImage; + if (!identical(belowLayerImage, oldBelow)) { + belowLayerImage?.dispose(); + } + if (!identical(aboveLayerImage, oldAbove)) { + aboveLayerImage?.dispose(); + } + Future.microtask(() async { + final latestState = controller.activeDocumentState; + if (latestState == null) return; + await bake( + controller, + latestState, + viewportSize: viewportSize, + pixelRatio: pixelRatio, + reset: reset, + resetAllLayers: resetAllLayers, + ); + }); + return; + } + + final newViewport = cameraViewport.bake( + height: size.height, + width: size.width, + pixelRatio: ratio, + resolution: resolution, + scale: transform.size, + x: renderTransform.position.dx, + y: renderTransform.position.dy, + image: newImage, + bakedElements: renderers, + unbakedElements: newlyUnbaked, + visibleElements: visibleElements, + visibleUnbakedElements: newlyUnbaked + .where((renderer) => renderer.isVisible(rect)) + .toList(), + belowLayerImage: belowLayerImage, + aboveLayerImage: aboveLayerImage, + rendererStates: allRendererStates, + invisibleLayers: invisibleLayers, + ); + rendererCubit.setViewport(newViewport); + }); + + Future renderImage( + EditorController controller, + NoteData document, + DocumentPage page, + DocumentInfo info, + ImageExportOptions options, { + CameraViewport? cameraViewport, + Set? invisibleLayers, + DocumentLoaded? docState, + }) async { + final rendererCubit = this; + final realWidth = (options.width * options.quality).ceil(); + final realHeight = (options.height * options.quality).ceil(); + final realZoom = options.scale; + if (realWidth <= 0 || realHeight <= 0) { + return null; + } + final size = Size(options.width, options.height); + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + canvas.scale(options.quality); + final viewport = + cameraViewport ?? + rendererCubit.state.cameraViewport.unbake( + unbakedElements: rendererCubit.renderers, + ); + final transform = CameraTransform( + options.quality, + Offset(options.x, options.y), + realZoom, + ); + final hiddenRenderers = >[]; + if (docState != null) { + final exportRect = Rect.fromLTWH( + options.x, + options.y, + options.width, + options.height, + ); + for (final renderer in viewport.unbakedElements) { + if (renderer.isVisible(exportRect)) { + final wasInitialized = rendererCubit.initializedElements.contains( + renderer, + ); + if (!wasInitialized) { + await renderer.onVisible(controller, docState, transform, size); + hiddenRenderers.add(renderer); + } + } + } + } + final painter = ViewPainter( + document, + page, + info, + renderBackground: options.renderBackground, + invisibleLayers: invisibleLayers, + cameraViewport: viewport, + transform: transform, + ); + painter.paint(canvas, size); + for (final renderer in hiddenRenderers) { + await renderer.onHidden(controller, docState!, transform, size); + } + final picture = recorder.endRecording(); + ui.Image? image; + try { + image = await picture.toImage(realWidth, realHeight); + } finally { + picture.dispose(); + } + return image; + } + + Future render( + EditorController controller, + NoteData document, + DocumentPage page, + DocumentInfo info, + ImageExportOptions options, { + CameraViewport? cameraViewport, + Set? invisibleLayers, + DocumentLoaded? docState, + }) async { + final image = await renderImage( + controller, + document, + page, + info, + options, + cameraViewport: cameraViewport, + invisibleLayers: invisibleLayers, + docState: docState, + ); + ByteData? bytes; + try { + bytes = await image?.toByteData(format: ui.ImageByteFormat.png); + } finally { + image?.dispose(); + } + return bytes; + } + + XmlDocument renderSVG( + NoteData document, + DocumentPage page, + SvgExportOptions options, { + Set? invisibleLayers, + }) { + final rendererCubit = this; + final xml = XmlDocument(); + xml.createElement( + 'svg', + attributes: { + 'xmlns': 'http://www.w3.org/2000/svg', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + 'version': '1.1', + 'width': '${options.width}px', + 'height': '${options.height}px', + 'viewBox': + '${options.x} ${options.y} ${options.width} ${options.height}', + }, + ); + + final rect = Rect.fromLTWH( + options.x, + options.y, + options.width.toDouble(), + options.height.toDouble(), + ); + if (options.renderBackground) { + for (final e in rendererCubit.state.cameraViewport.backgrounds) { + e.buildSvg(xml, document, page, rect); + } + } + for (var e in rendererCubit.renderers) { + if ((invisibleLayers?.contains(e.layer) ?? false) || !e.isVisible(rect)) { + continue; + } + e.buildSvg(xml, document, page, rect); + } + return xml; + } + + Future unbake( + EditorController controller, + DocumentLoaded blocState, { + List>? backgrounds, + List>? unbakedElements, + }) async { + final rendererCubit = this; + final transformCubit = controller.transformCubit; + final elementsToCheck = unbakedElements ?? rendererCubit.renderers; + final oldViewport = rendererCubit.state.cameraViewport; + final newViewport = oldViewport.unbake( + unbakedElements: unbakedElements, + visibleElements: elementsToCheck + .where( + (e) => e.isVisible(rendererCubit.getViewportRect(transformCubit)), + ) + .toList(), + backgrounds: backgrounds, + ); + await rendererCubit.updateOnVisible(controller, newViewport, blocState); + rendererCubit.setViewport(newViewport); + } + + Future replaceUnbaked( + EditorController controller, + DocumentLoaded blocState, + List> unbakedElements, { + List>? backgrounds, + }) async { + final rendererCubit = this; + final transformCubit = controller.transformCubit; + final visibleElements = unbakedElements + .where( + (e) => e.isVisible(rendererCubit.getViewportRect(transformCubit)), + ) + .toList(); + final newViewport = rendererCubit.state.cameraViewport.replaceUnbaked( + unbakedElements, + visibleElements: visibleElements, + visibleUnbakedElements: visibleElements, + backgrounds: backgrounds, + ); + await rendererCubit.updateOnVisible(controller, newViewport, blocState); + rendererCubit.setViewport(newViewport); + } + + Future loadElements( + EditorController controller, + DocumentState docState, { + bool reset = false, + }) async { + final rendererCubit = this; + final transformCubit = controller.transformCubit; + if (docState is! DocumentLoaded) return; + final document = docState.data; + final assetService = docState.assetService; + final page = docState.page; + var existing = rendererCubit.renderers; + if (reset) { + for (var e in existing) { + rendererCubit.initializedElements.remove(e); + e.dispose(); + } + existing = []; + } + final elements = page.layers + .where((e) => !docState.invisibleLayers.contains(e.id)) + .expand((l) => l.content.map((e) => (e, l.id))) + .toList(); + final elementKeys = elements + .map((element) => (element.$1, element.$2)) + .toSet(); + final existingByKey = { + for (final renderer in existing) + (renderer.element, renderer.layer): renderer, + }; + final reusable = >[]; + final reusableKeys = <(PadElement, String?)>{}; + for (final element in elements) { + final key = (element.$1, element.$2); + final renderer = existingByKey[key]; + if (renderer != null) { + reusable.add(renderer); + reusableKeys.add(key); + } + } + final dropped = existing + .where( + (renderer) => + !elementKeys.contains((renderer.element, renderer.layer)), + ) + .toList(); + for (final e in dropped) { + rendererCubit.initializedElements.remove(e); + e.dispose(); + } + final newRenderers = elements + .where((e) => !reusableKeys.contains((e.$1, e.$2))) + .map((e) => Renderer.fromInstance(e.$1, e.$2)) + .toList(); + await Future.wait( + newRenderers.map( + (e) async => + await e.setup(transformCubit, document, assetService, page), + ), + ); + // Build layer index map for O(1) lookups instead of O(n) indexOf calls + final layersList = page.layers.map((e) => e.id).toList(); + final layerIndexMap = {}; + for (var i = 0; i < layersList.length; i++) { + layerIndexMap[layersList[i]] = i; + } + + // Build element index map for O(1) lookups + final elementIndexMap = {}; + for (var i = 0; i < elements.length; i++) { + elementIndexMap[elements[i].$1] = i; + } + + final combined = [...reusable, ...newRenderers] + ..sort((a, b) { + final layerA = layerIndexMap[a.layer] ?? layersList.length; + final layerB = layerIndexMap[b.layer] ?? layersList.length; + if (layerA != layerB) return layerA.compareTo(layerB); + final indexA = elementIndexMap[a.element] ?? -1; + final indexB = elementIndexMap[b.element] ?? -1; + return indexA.compareTo(indexB); + }); + final backgrounds = page.backgrounds.map(Renderer.fromInstance).toList(); + await Future.wait( + backgrounds.map( + (e) async => + await e.setup(transformCubit, document, assetService, page), + ), + ); + final rect = rendererCubit.getViewportRect(transformCubit); + final visibleElements = combined.where((e) => e.isVisible(rect)).toList(); + final oldViewport = rendererCubit.state.cameraViewport; + final newViewport = oldViewport.unbake( + unbakedElements: combined, + visibleElements: visibleElements, + backgrounds: backgrounds, + ); + await rendererCubit.updateOnVisible(controller, newViewport, docState); + controller.saveCubit.setSaveState( + location: + controller.saveCubit.state.embedding?.location ?? + controller.saveCubit.state.location, + ); + rendererCubit.setViewport(newViewport); + } + + Future addUnbaked( + EditorController controller, + DocumentLoaded blocState, + List> unbakedElements, [ + List>? visibleElements, + ]) async { + final rendererCubit = this; + final transformCubit = controller.transformCubit; + final rect = rendererCubit.getViewportRect(transformCubit); + visibleElements ??= unbakedElements + .where((e) => e.isVisible(rect)) + .toList(); + final nextUnbaked = [ + ...rendererCubit.state.cameraViewport.unbakedElements, + ...unbakedElements, + ]; + final newViewport = rendererCubit.state.cameraViewport.withUnbaked( + nextUnbaked, + visibleElements: [ + ...rendererCubit.state.cameraViewport.visibleElements, + ...visibleElements, + ], + visibleUnbakedElements: [ + ...rendererCubit.state.cameraViewport.visibleUnbakedElements, + ...visibleElements, + ], + ); + await rendererCubit.updateOnVisible(controller, newViewport, blocState); + rendererCubit.setViewport(newViewport); + } + + Future renderPDF( + EditorController controller, + DocumentLoaded docState, { + required List areas, + bool renderBackground = true, + void Function(double progress)? onProgress, + Set? invisibleLayers, + }) async { + final transformCubit = controller.transformCubit; + var name = docState.metadata.name; + if (name.isEmpty) { + name = 'document'; + } + final pdf = await PdfDocument.createNew(sourceName: '$name.pdf'); + final document = docState.data; + final info = docState.info; + final pages = []; + final documents = []; + for (var i = 0; i < areas.length; i++) { + onProgress?.call(i / areas.length); + final preset = areas[i]; + final areaName = preset.name; + final quality = preset.quality; + final currentOpened = docState.pageName == preset.page; + final page = currentOpened + ? docState.page + : document.getPage(preset.page); + final area = preset.area ?? page?.getAreaByName(areaName); + if (area == null || page == null) { + continue; + } + final image = await renderImage( + controller, + document, + page, + info, + ImageExportOptions( + width: area.width, + height: area.height, + x: area.position.x, + y: area.position.y, + quality: quality, + renderBackground: renderBackground, + ), + cameraViewport: await CameraViewport.build( + transformCubit, + document, + docState.assetService, + page, + ), + docState: docState, + invisibleLayers: invisibleLayers ?? docState.invisibleLayers, + ); + if (image == null) continue; + final imgImage = await convertFlutterUiToImage(image); + final pdfImage = await compute( + (image) => img.JpegEncoder().encode(image), + imgImage, + ); + final imageDoc = await PdfDocument.createFromJpegData( + pdfImage, + width: area.width, + height: area.height, + sourceName: '$name-$areaName.jpg', + ); + pages.addAll(imageDoc.pages); + image.dispose(); + documents.add(imageDoc); + } + onProgress?.call(1.0); + pdf.pages = pages; + final bytes = await pdf.encodePdf(); + pdf.dispose(); + for (final doc in documents) { + doc.dispose(); + } + return bytes; + } + + Future disposeRuntime() async { + await _transformSubscription?.cancel(); + _transformSubscription = null; + _transformDebounceTimer?.cancel(); + _transformDebounceTimer = null; + _controller = null; + delayedBakeRunner.cancel(); + await delayedBakeRunner.disposeAndWait(); + initializedElements.clear(); + state.cameraViewport.disposeImages(); + for (final renderer in renderers) { + renderer.dispose(); + } + } + + @override + Future close() async { + await _transformSubscription?.cancel(); + _transformSubscription = null; + _transformDebounceTimer?.cancel(); + _transformDebounceTimer = null; + _controller = null; + return super.close(); + } +} diff --git a/app/lib/cubits/editor_runtime.dart b/app/lib/cubits/editor_runtime.dart index 1dfec60b2f84..d71cf45f67fa 100644 --- a/app/lib/cubits/editor_runtime.dart +++ b/app/lib/cubits/editor_runtime.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math'; import 'dart:ui' as ui; @@ -26,2332 +27,52 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:image/image.dart' as img; import 'package:lw_file_system/lw_file_system.dart'; +import 'package:material_leap/helpers.dart'; import 'package:pdfrx/pdfrx.dart'; import 'package:synchronized/synchronized.dart'; import 'package:xml/xml.dart'; part 'editor_runtime.freezed.dart'; +part 'editor_renderer.dart'; +part 'editor_tool.dart'; +part 'editor_input.dart'; +part 'document_save.dart'; +part 'editor_view.dart'; Future _toFile((NoteData, bool) args) async { return args.$1.toFile(isTextBased: args.$2); } -enum SaveState { saved, saving, unsaved, absoluteRead } - -enum HideState { visible, keyboard, touch } - -enum RendererState { visible, temporary, hidden } - -enum TemporaryState { allowClick, removeAfterClick, removeAfterRelease } - -@Freezed(equal: false) -sealed class RendererRuntimeState with _$RendererRuntimeState { - const RendererRuntimeState._(); - - const factory RendererRuntimeState({ - @Default(CameraViewport.unbaked()) CameraViewport cameraViewport, - @Default({}) Map rendererStates, - @Default({}) Map? temporaryRendererStates, - }) = _RendererRuntimeState; - - Map get allRendererStates => { - ...rendererStates, - ...?temporaryRendererStates, - }; -} - -class RendererCubit extends Cubit { - RendererCubit( - this.settingsCubit, [ - super.initial = const RendererRuntimeState(), - ]); - - final SettingsCubit settingsCubit; - - final initializedElements = >{}; - final bakeLock = Lock(); - final delayedBakeRunner = CoalescedAsyncRunner( - delay: const Duration(milliseconds: 100), - ); - - void cancelDelayedBake() { - delayedBakeRunner.cancel(); - } - - void replace(RendererRuntimeState state) => emit(state); - - void setViewport(CameraViewport cameraViewport) => - emit(state.copyWith(cameraViewport: cameraViewport)); - - void setRendererStates({ - Map? rendererStates, - Map? temporaryRendererStates, - }) => emit( - state.copyWith( - rendererStates: rendererStates ?? state.rendererStates, - temporaryRendererStates: - temporaryRendererStates ?? state.temporaryRendererStates, - ), - ); - - List> get renderers => - List>.from(state.cameraViewport.bakedElements) - ..addAll(state.cameraViewport.unbakedElements); - - Renderer? getRenderer(PadElement element) => - renderers.firstWhereOrNull((renderer) => renderer.element == element); - - bool sameRendererList( - List> a, - List> b, - ) { - if (identical(a, b)) return true; - if (a.length != b.length) return false; - for (var i = 0; i < a.length; i++) { - if (!identical(a[i], b[i])) return false; - } - return true; - } - - void invalidateRenderers(Iterable> renderers) { - initializedElements.removeAll(renderers); - } - - Rect getViewportRect(TransformCubit transformCubit, {Size? viewportSize}) { - var size = viewportSize ?? state.cameraViewport.toSize(); - final transform = transformCubit.state; - final resolution = settingsCubit.state.renderResolution; - final friction = transform.friction; - final realWidth = size.width / transform.size; - final realHeight = size.height / transform.size; - Rect rect = Rect.fromLTWH( - transform.position.dx, - transform.position.dy, - realWidth, - realHeight, - ); - if (friction != null) { - final beginPosition = transform.position - friction.beginOffset; - final topLeft = Offset( - min(transform.position.dx, beginPosition.dx), - min(transform.position.dy, beginPosition.dy), - ); - final frictionSize = Size( - realWidth + (friction.beginOffset.dx * transform.size).abs(), - realHeight + (friction.beginOffset.dy * transform.size).abs(), - ); - rect = topLeft & frictionSize; - } - return _snapViewportRect(rect, size, transform, resolution); - } - - Rect _snapViewportRect( - Rect rect, - Size size, - CameraTransform transform, - RenderResolution resolution, - ) { - final screenRect = Rect.fromPoints( - transform.globalToLocal(rect.topLeft), - transform.globalToLocal(rect.bottomRight), - ); - final snappedRect = _expandScreenRect( - Rect.fromLTRB( - screenRect.left.floorToDouble(), - screenRect.top.floorToDouble(), - screenRect.right.ceilToDouble(), - screenRect.bottom.ceilToDouble(), - ), - Size( - (size.width * resolution.multiplier).ceilToDouble(), - (size.height * resolution.multiplier).ceilToDouble(), - ), - ); - return Rect.fromPoints( - transform.localToGlobal(snappedRect.topLeft), - transform.localToGlobal(snappedRect.bottomRight), - ); - } - - Rect _expandScreenRect(Rect rect, Size minimumSize) { - final dx = max(0.0, minimumSize.width - rect.width); - final dy = max(0.0, minimumSize.height - rect.height); - return Rect.fromLTRB( - rect.left - (dx / 2).floorToDouble(), - rect.top - (dy / 2).floorToDouble(), - rect.right + (dx / 2).ceilToDouble(), - rect.bottom + (dy / 2).ceilToDouble(), - ); - } - - bool rectContains(Rect outer, Rect inner) { - const tolerance = precisionErrorTolerance; - return outer.left <= inner.left + tolerance && - outer.top <= inner.top + tolerance && - outer.right >= inner.right - tolerance && - outer.bottom >= inner.bottom - tolerance; - } - - Rect getPageRect({Set? invisibleLayers}) { - Rect? rect; - for (final renderer in renderers) { - final rendererRect = renderer.expandedRect; - if (rendererRect == null) continue; - if (invisibleLayers?.contains(renderer.layer) ?? false) continue; - rect = rect?.expandToInclude(rendererRect) ?? rendererRect; - } - return rect ?? Rect.zero; - } - - Future updateVisibleElements( - EditorController controller, - DocumentBloc? bloc, - ) async { - if (controller.isClosed) return; - final unbaked = state.cameraViewport.unbakedElements; - final baked = state.cameraViewport.bakedElements; - - final rect = getViewportRect(controller.transformCubit); - final currentVisible = state.cameraViewport.visibleElements; - final currentVisibleUnbaked = state.cameraViewport.visibleUnbakedElements; - - final visibleUnbaked = unbaked.where((e) => e.isVisible(rect)).toList(); - final visible = >[ - ...baked.where((e) => e.isVisible(rect)), - ...visibleUnbaked, - ]; - - if (sameRendererList(visible, currentVisible) && - sameRendererList(visibleUnbaked, currentVisibleUnbaked)) { - return; - } - - final newViewport = state.cameraViewport.withUnbaked( - unbaked, - visibleElements: visible, - visibleUnbakedElements: visibleUnbaked, - ); - - final docState = bloc?.state; - if (docState is DocumentLoaded) { - await updateOnVisible(controller, newViewport, docState); - if (!controller.isClosed && bloc != null && !bloc.isClosed) { - bloc.delayedBake(); - } - } - - if (controller.isClosed) return; - - setViewport(newViewport); - } - - Future updateOnVisible( - EditorController controller, - CameraViewport newViewport, - DocumentLoaded blocState, { - CameraTransform? renderTransform, - ui.Size? targetSize, - }) async { - final newVisibleList = newViewport.visibleElements; - final nextVisibleSet = newVisibleList.toSet(); - - final newVisible = newVisibleList - .where((e) => !initializedElements.contains(e)) - .toList(); - - final newlyHidden = initializedElements - .where((e) => !nextVisibleSet.contains(e)) - .toList(); - - if (newVisible.isEmpty && newlyHidden.isEmpty) return; - - final transform = renderTransform ?? controller.transformCubit.state; - final size = targetSize ?? newViewport.toSize(); - - initializedElements.removeAll(newlyHidden); - - if (newVisible.isNotEmpty) { - talker.verbose('Updating visible elements: ${newVisible.length} new'); - final initialized = await Future.wait( - newVisible.map((element) async { - try { - await Future.sync( - () => element.onVisible(controller, blocState, transform, size), - ); - return element; - } catch (error, stackTrace) { - talker.error( - 'Failed to initialize visible renderer $element', - error, - stackTrace, - ); - } - return null; - }), - ); - initializedElements.addAll(initialized.nonNulls); - } - - if (newlyHidden.isNotEmpty) { - await Future.wait( - newlyHidden.map( - (element) async => - await element.onHidden(controller, blocState, transform, size), - ), - ); - } - } - - Future delayedBake( - EditorController controller, - DocumentLoaded blocState, { - ui.Size? viewportSize, - double? pixelRatio, - bool reset = false, - bool testTransform = false, - }) => delayedBakeRunner.schedule(() async { - final newTransform = controller.transformCubit.state; - final viewport = state.cameraViewport; - - if (testTransform && - newTransform.size == viewport.scale && - newTransform.position == viewport.toOffset()) { - return; - } - - await controller.rendererCubit.bake( - controller, - blocState, - viewportSize: viewportSize, - pixelRatio: pixelRatio, - reset: reset, - ); - }); - - Future bake( - EditorController controller, - DocumentLoaded blocState, { - Size? viewportSize, - double? pixelRatio, - bool reset = false, - bool resetAllLayers = false, - }) => bakeLock.synchronized(() async { - final rendererCubit = this; - final transformCubit = controller.transformCubit; - final settingsCubit = controller.settingsCubit; - if (controller.isClosed) return; - var cameraViewport = rendererCubit.state.cameraViewport; - final startTransform = transformCubit.state; - final startViewport = cameraViewport; - final resolution = settingsCubit.state.renderResolution; - var size = viewportSize ?? cameraViewport.toSize(); - final ratio = pixelRatio ?? cameraViewport.pixelRatio; - if (size.height <= 0 || size.width <= 0) { - return; - } - if (viewportSize == null) { - size /= resolution.multiplier; - } - var transform = transformCubit.state; - var renderers = List>.from(rendererCubit.renderers); - final recorder = ui.PictureRecorder(); - final canvas = ui.Canvas(recorder); - final rect = rendererCubit.getViewportRect( - transformCubit, - viewportSize: size, - ); - size = rect.size * transform.size; - final renderTransform = transform.improve(resolution, rect); - final document = blocState.data; - final page = blocState.page; - final info = blocState.info; - final imageWidth = (size.width * ratio).ceil(); - final imageHeight = (size.height * ratio).ceil(); - var allRendererStates = rendererCubit.state.allRendererStates; - final rendererStatesChanged = !mapEquals( - allRendererStates, - cameraViewport.rendererStates, - ); - if (!rendererStatesChanged) { - allRendererStates = cameraViewport.rendererStates; - } - final invisibleLayers = blocState.invisibleLayers; - final viewportAlreadyCoversRect = - cameraViewport.image != null && - cameraViewport.scale == transform.size && - cameraViewport.resolution == resolution && - cameraViewport.pixelRatio == ratio && - !rendererStatesChanged && - setEquals(cameraViewport.invisibleLayers, invisibleLayers) && - rendererCubit.rectContains(cameraViewport.toRect(), rect); - final viewChanged = - !viewportAlreadyCoversRect && - (cameraViewport.width != size.width.ceil() || - cameraViewport.height != size.height.ceil() || - cameraViewport.pixelRatio != ratio || - cameraViewport.resolution != resolution || - cameraViewport.x != renderTransform.position.dx || - cameraViewport.y != renderTransform.position.dy || - cameraViewport.scale != transform.size || - rendererStatesChanged || - !setEquals(cameraViewport.invisibleLayers, invisibleLayers)); - reset = reset || viewChanged; - resetAllLayers = resetAllLayers || viewChanged; - if (cameraViewport.unbakedElements.isEmpty && !reset) return; - final currentLayer = blocState.currentLayer; - List> visibleElements; - final oldVisible = cameraViewport.visibleElements; - final oldVisibleSet = oldVisible.toSet(); - talker.verbose( - 'Baking viewport (reset: $reset, viewChanged: $viewChanged, ' - 'rendererStatesChanged: $rendererStatesChanged)', - ); - - if (reset) { - visibleElements = renderers - .where((renderer) => renderer.isVisible(rect)) - .toList(); - } else { - visibleElements = List.from(oldVisible) - ..addAll( - cameraViewport.unbakedElements.where( - (renderer) => - !oldVisibleSet.contains(renderer) && renderer.isVisible(rect), - ), - ); - } - - final visibleElementsSet = visibleElements.toSet(); - - await rendererCubit.updateOnVisible( - controller, - cameraViewport.unbake(visibleElements: visibleElements), - blocState, - renderTransform: renderTransform, - targetSize: size, - ); - - canvas.scale(ratio); - - if (viewChanged && visibleElements.isNotEmpty) { - await Future.wait( - visibleElements.map( - (e) async => - await e.updateView(controller, blocState, renderTransform, size), - ), - ); - } - - // Wait one frame - await Future.delayed(const Duration(milliseconds: 1)); - - ViewPainter( - document, - page, - info, - transform: renderTransform, - cameraViewport: reset - ? cameraViewport.unbake( - rendererStates: allRendererStates, - unbakedElements: visibleElements - .where((e) => currentLayer == e.layer) - .toList(), - visibleElements: visibleElements, - ) - : cameraViewport, - renderBackground: false, - renderBaked: !reset, - renderBakedLayers: false, - invisibleLayers: invisibleLayers, - ).paint(canvas, size); - - final picture = recorder.endRecording(); - ui.Image newImage; - try { - newImage = await picture.toImage(imageWidth, imageHeight); - } finally { - picture.dispose(); - } - - var belowLayerImage = cameraViewport.belowLayerImage; - var aboveLayerImage = cameraViewport.aboveLayerImage; - - if (resetAllLayers) { - final belowLayerRecorder = ui.PictureRecorder(); - final belowLayerCanvas = ui.Canvas(belowLayerRecorder); - belowLayerCanvas.scale(ratio); - final aboveLayerRecorder = ui.PictureRecorder(); - final aboveLayerCanvas = ui.Canvas(aboveLayerRecorder); - aboveLayerCanvas.scale(ratio); - final belowLayers = {}, aboveLayers = {}; - bool above = false; - for (final layer in page.layers) { - if (layer.id == currentLayer) { - above = true; - continue; - } - final layerId = layer.id; - if (layerId == null) continue; - if (above) { - aboveLayers.add(layerId); - } else { - belowLayers.add(layerId); - } - } - - ViewPainter( - document, - page, - info, - transform: renderTransform, - cameraViewport: cameraViewport.unbake( - rendererStates: allRendererStates, - unbakedElements: visibleElements - .where((e) => e.layer != null && belowLayers.contains(e.layer)) - .toList(), - visibleElements: visibleElements, - ), - renderBackground: false, - renderBaked: false, - invisibleLayers: invisibleLayers, - ).paint(belowLayerCanvas, size); - ViewPainter( - document, - page, - info, - transform: renderTransform, - cameraViewport: cameraViewport.unbake( - rendererStates: allRendererStates, - unbakedElements: visibleElements - .where((e) => e.layer != null && aboveLayers.contains(e.layer)) +void _sendNetworkingState( + EditorController controller, { + List>? foregrounds, + Offset? cursor, +}) { + cursor ??= controller.inputCubit.state.lastPosition ?? Offset.zero; + controller.networkingService.sendUser( + NetworkingUser( + cursor: controller.transformCubit.state.localToGlobal(cursor).toPoint(), + foreground: + (foregrounds ?? controller.toolCubit.state.getAllForegrounds(false)) + .map((e) => e.element) + .whereType() .toList(), - visibleElements: visibleElements, - ), - renderBackground: false, - renderBaked: false, - invisibleLayers: invisibleLayers, - ).paint(aboveLayerCanvas, size); - - final belowPicture = belowLayerRecorder.endRecording(); - final abovePicture = aboveLayerRecorder.endRecording(); - try { - final result = await Future.wait([ - belowPicture.toImage(imageWidth, imageHeight), - abovePicture.toImage(imageWidth, imageHeight), - ]); - belowLayerImage = result[0]; - aboveLayerImage = result[1]; - } finally { - belowPicture.dispose(); - abovePicture.dispose(); - } - } - - final bakedElementsSet = cameraViewport.bakedElements - .map((e) => e.element) - .toSet(); - final unbakedElementsSet = cameraViewport.unbakedElements - .map((e) => e.element) - .toSet(); - - final newlyUnbaked = - (reset - ? rendererCubit.renderers - : rendererCubit.state.cameraViewport.unbakedElements) - .where( - (element) => - !bakedElementsSet.contains(element.element) && - !unbakedElementsSet.contains(element.element) && - !visibleElementsSet.contains(element), - ) - .toList(); - - if (controller.isClosed) return; - - // If state changed while baking (e.g. fast move submitted a newer viewport), - // this bake output is stale and must not overwrite the latest viewport. - final currentViewport = rendererCubit.state.cameraViewport; - final currentTransform = transformCubit.state; - if (!identical(currentViewport, startViewport) || - currentTransform != startTransform) { - newImage.dispose(); - final oldBelow = startViewport.belowLayerImage; - final oldAbove = startViewport.aboveLayerImage; - if (!identical(belowLayerImage, oldBelow)) { - belowLayerImage?.dispose(); - } - if (!identical(aboveLayerImage, oldAbove)) { - aboveLayerImage?.dispose(); - } - Future.microtask(() async { - final latestState = controller.activeDocumentState; - if (latestState == null) return; - await bake( - controller, - latestState, - viewportSize: viewportSize, - pixelRatio: pixelRatio, - reset: reset, - resetAllLayers: resetAllLayers, - ); - }); - return; - } - - final newViewport = cameraViewport.bake( - height: size.height, - width: size.width, - pixelRatio: ratio, - resolution: resolution, - scale: transform.size, - x: renderTransform.position.dx, - y: renderTransform.position.dy, - image: newImage, - bakedElements: renderers, - unbakedElements: newlyUnbaked, - visibleElements: visibleElements, - visibleUnbakedElements: newlyUnbaked - .where((renderer) => renderer.isVisible(rect)) - .toList(), - belowLayerImage: belowLayerImage, - aboveLayerImage: aboveLayerImage, - rendererStates: allRendererStates, - invisibleLayers: invisibleLayers, - ); - rendererCubit.setViewport(newViewport); - }); - - Future renderImage( - EditorController controller, - NoteData document, - DocumentPage page, - DocumentInfo info, - ImageExportOptions options, { - CameraViewport? cameraViewport, - Set? invisibleLayers, - DocumentLoaded? docState, - }) async { - final rendererCubit = this; - final realWidth = (options.width * options.quality).ceil(); - final realHeight = (options.height * options.quality).ceil(); - final realZoom = options.scale; - if (realWidth <= 0 || realHeight <= 0) { - return null; - } - final size = Size(options.width, options.height); - final recorder = ui.PictureRecorder(); - final canvas = Canvas(recorder); - canvas.scale(options.quality); - final viewport = - cameraViewport ?? - rendererCubit.state.cameraViewport.unbake( - unbakedElements: rendererCubit.renderers, - ); - final transform = CameraTransform( - options.quality, - Offset(options.x, options.y), - realZoom, - ); - final hiddenRenderers = >[]; - if (docState != null) { - final exportRect = Rect.fromLTWH( - options.x, - options.y, - options.width, - options.height, - ); - for (final renderer in viewport.unbakedElements) { - if (renderer.isVisible(exportRect)) { - final wasInitialized = rendererCubit.initializedElements.contains( - renderer, - ); - if (!wasInitialized) { - await renderer.onVisible(controller, docState, transform, size); - hiddenRenderers.add(renderer); - } - } - } - } - final painter = ViewPainter( - document, - page, - info, - renderBackground: options.renderBackground, - invisibleLayers: invisibleLayers, - cameraViewport: viewport, - transform: transform, - ); - painter.paint(canvas, size); - for (final renderer in hiddenRenderers) { - await renderer.onHidden(controller, docState!, transform, size); - } - final picture = recorder.endRecording(); - ui.Image? image; - try { - image = await picture.toImage(realWidth, realHeight); - } finally { - picture.dispose(); - } - return image; - } - - Future render( - EditorController controller, - NoteData document, - DocumentPage page, - DocumentInfo info, - ImageExportOptions options, { - CameraViewport? cameraViewport, - Set? invisibleLayers, - DocumentLoaded? docState, - }) async { - final image = await renderImage( - controller, - document, - page, - info, - options, - cameraViewport: cameraViewport, - invisibleLayers: invisibleLayers, - docState: docState, - ); - ByteData? bytes; - try { - bytes = await image?.toByteData(format: ui.ImageByteFormat.png); - } finally { - image?.dispose(); - } - return bytes; - } - - XmlDocument renderSVG( - NoteData document, - DocumentPage page, - SvgExportOptions options, { - Set? invisibleLayers, - }) { - final rendererCubit = this; - final xml = XmlDocument(); - xml.createElement( - 'svg', - attributes: { - 'xmlns': 'http://www.w3.org/2000/svg', - 'xmlns:xlink': 'http://www.w3.org/1999/xlink', - 'version': '1.1', - 'width': '${options.width}px', - 'height': '${options.height}px', - 'viewBox': - '${options.x} ${options.y} ${options.width} ${options.height}', - }, - ); - - final rect = Rect.fromLTWH( - options.x, - options.y, - options.width.toDouble(), - options.height.toDouble(), - ); - if (options.renderBackground) { - for (final e in rendererCubit.state.cameraViewport.backgrounds) { - e.buildSvg(xml, document, page, rect); - } - } - for (var e in rendererCubit.renderers) { - if ((invisibleLayers?.contains(e.layer) ?? false) || !e.isVisible(rect)) { - continue; - } - e.buildSvg(xml, document, page, rect); - } - return xml; - } - - Future unbake( - EditorController controller, - DocumentLoaded blocState, { - List>? backgrounds, - List>? unbakedElements, - }) async { - final rendererCubit = this; - final transformCubit = controller.transformCubit; - final elementsToCheck = unbakedElements ?? rendererCubit.renderers; - final oldViewport = rendererCubit.state.cameraViewport; - final newViewport = oldViewport.unbake( - unbakedElements: unbakedElements, - visibleElements: elementsToCheck - .where( - (e) => e.isVisible(rendererCubit.getViewportRect(transformCubit)), - ) - .toList(), - backgrounds: backgrounds, - ); - await rendererCubit.updateOnVisible(controller, newViewport, blocState); - rendererCubit.setViewport(newViewport); - } - - Future replaceUnbaked( - EditorController controller, - DocumentLoaded blocState, - List> unbakedElements, { - List>? backgrounds, - }) async { - final rendererCubit = this; - final transformCubit = controller.transformCubit; - final visibleElements = unbakedElements - .where( - (e) => e.isVisible(rendererCubit.getViewportRect(transformCubit)), - ) - .toList(); - final newViewport = rendererCubit.state.cameraViewport.replaceUnbaked( - unbakedElements, - visibleElements: visibleElements, - visibleUnbakedElements: visibleElements, - backgrounds: backgrounds, - ); - await rendererCubit.updateOnVisible(controller, newViewport, blocState); - rendererCubit.setViewport(newViewport); - } - - Future loadElements( - EditorController controller, - DocumentState docState, { - bool reset = false, - }) async { - final rendererCubit = this; - final transformCubit = controller.transformCubit; - if (docState is! DocumentLoaded) return; - final document = docState.data; - final assetService = docState.assetService; - final page = docState.page; - var existing = rendererCubit.renderers; - if (reset) { - for (var e in existing) { - rendererCubit.initializedElements.remove(e); - e.dispose(); - } - existing = []; - } - final elements = page.layers - .where((e) => !docState.invisibleLayers.contains(e.id)) - .expand((l) => l.content.map((e) => (e, l.id))) - .toList(); - final elementKeys = elements - .map((element) => (element.$1, element.$2)) - .toSet(); - final existingByKey = { - for (final renderer in existing) - (renderer.element, renderer.layer): renderer, - }; - final reusable = >[]; - final reusableKeys = <(PadElement, String?)>{}; - for (final element in elements) { - final key = (element.$1, element.$2); - final renderer = existingByKey[key]; - if (renderer != null) { - reusable.add(renderer); - reusableKeys.add(key); - } - } - final dropped = existing - .where( - (renderer) => - !elementKeys.contains((renderer.element, renderer.layer)), - ) - .toList(); - for (final e in dropped) { - rendererCubit.initializedElements.remove(e); - e.dispose(); - } - final newRenderers = elements - .where((e) => !reusableKeys.contains((e.$1, e.$2))) - .map((e) => Renderer.fromInstance(e.$1, e.$2)) - .toList(); - await Future.wait( - newRenderers.map( - (e) async => - await e.setup(transformCubit, document, assetService, page), - ), - ); - // Build layer index map for O(1) lookups instead of O(n) indexOf calls - final layersList = page.layers.map((e) => e.id).toList(); - final layerIndexMap = {}; - for (var i = 0; i < layersList.length; i++) { - layerIndexMap[layersList[i]] = i; - } - - // Build element index map for O(1) lookups - final elementIndexMap = {}; - for (var i = 0; i < elements.length; i++) { - elementIndexMap[elements[i].$1] = i; - } - - final combined = [...reusable, ...newRenderers] - ..sort((a, b) { - final layerA = layerIndexMap[a.layer] ?? layersList.length; - final layerB = layerIndexMap[b.layer] ?? layersList.length; - if (layerA != layerB) return layerA.compareTo(layerB); - final indexA = elementIndexMap[a.element] ?? -1; - final indexB = elementIndexMap[b.element] ?? -1; - return indexA.compareTo(indexB); - }); - final backgrounds = page.backgrounds.map(Renderer.fromInstance).toList(); - await Future.wait( - backgrounds.map( - (e) async => - await e.setup(transformCubit, document, assetService, page), - ), - ); - final rect = rendererCubit.getViewportRect(transformCubit); - final visibleElements = combined.where((e) => e.isVisible(rect)).toList(); - final oldViewport = rendererCubit.state.cameraViewport; - final newViewport = oldViewport.unbake( - unbakedElements: combined, - visibleElements: visibleElements, - backgrounds: backgrounds, - ); - await rendererCubit.updateOnVisible(controller, newViewport, docState); - controller.saveCubit.setSaveState( - location: - controller.saveCubit.state.embedding?.location ?? - controller.saveCubit.state.location, - ); - rendererCubit.setViewport(newViewport); - } - - Future addUnbaked( - EditorController controller, - DocumentLoaded blocState, - List> unbakedElements, [ - List>? visibleElements, - ]) async { - final rendererCubit = this; - final transformCubit = controller.transformCubit; - final rect = rendererCubit.getViewportRect(transformCubit); - visibleElements ??= unbakedElements - .where((e) => e.isVisible(rect)) - .toList(); - final nextUnbaked = [ - ...rendererCubit.state.cameraViewport.unbakedElements, - ...unbakedElements, - ]; - final newViewport = rendererCubit.state.cameraViewport.withUnbaked( - nextUnbaked, - visibleElements: [ - ...rendererCubit.state.cameraViewport.visibleElements, - ...visibleElements, - ], - visibleUnbakedElements: [ - ...rendererCubit.state.cameraViewport.visibleUnbakedElements, - ...visibleElements, - ], - ); - await rendererCubit.updateOnVisible(controller, newViewport, blocState); - rendererCubit.setViewport(newViewport); - } - - Future renderPDF( - EditorController controller, - DocumentLoaded docState, { - required List areas, - bool renderBackground = true, - void Function(double progress)? onProgress, - Set? invisibleLayers, - }) async { - final transformCubit = controller.transformCubit; - var name = docState.metadata.name; - if (name.isEmpty) { - name = 'document'; - } - final pdf = await PdfDocument.createNew(sourceName: '$name.pdf'); - final document = docState.data; - final info = docState.info; - final pages = []; - final documents = []; - for (var i = 0; i < areas.length; i++) { - onProgress?.call(i / areas.length); - final preset = areas[i]; - final areaName = preset.name; - final quality = preset.quality; - final currentOpened = docState.pageName == preset.page; - final page = currentOpened - ? docState.page - : document.getPage(preset.page); - final area = preset.area ?? page?.getAreaByName(areaName); - if (area == null || page == null) { - continue; - } - final image = await renderImage( - controller, - document, - page, - info, - ImageExportOptions( - width: area.width, - height: area.height, - x: area.position.x, - y: area.position.y, - quality: quality, - renderBackground: renderBackground, - ), - cameraViewport: await CameraViewport.build( - transformCubit, - document, - docState.assetService, - page, - ), - docState: docState, - invisibleLayers: invisibleLayers ?? docState.invisibleLayers, - ); - if (image == null) continue; - final imgImage = await convertFlutterUiToImage(image); - final pdfImage = await compute( - (image) => img.JpegEncoder().encode(image), - imgImage, - ); - final imageDoc = await PdfDocument.createFromJpegData( - pdfImage, - width: area.width, - height: area.height, - sourceName: '$name-$areaName.jpg', - ); - pages.addAll(imageDoc.pages); - image.dispose(); - documents.add(imageDoc); - } - onProgress?.call(1.0); - pdf.pages = pages; - final bytes = await pdf.encodePdf(); - pdf.dispose(); - for (final doc in documents) { - doc.dispose(); - } - return bytes; - } - - Future disposeRuntime() async { - delayedBakeRunner.cancel(); - await delayedBakeRunner.disposeAndWait(); - initializedElements.clear(); - state.cameraViewport.disposeImages(); - for (final renderer in renderers) { - renderer.dispose(); - } - } -} - -@Freezed(equal: false) -sealed class ToolRuntimeState with _$ToolRuntimeState { - const ToolRuntimeState._(); - - const factory ToolRuntimeState({ - int? index, - required Handler handler, - Handler? temporaryHandler, - int? temporaryIndex, - @Default([]) List foregrounds, - Selection? selection, - @Default(false) bool pinned, - List? temporaryForegrounds, - @Default({}) Map> toggleableHandlers, - @Default([]) List networkingForegrounds, - @Default({}) Map> toggleableForegrounds, - @Default(MouseCursor.defer) MouseCursor cursor, - MouseCursor? temporaryCursor, - @Default(TemporaryState.allowClick) TemporaryState temporaryState, - PreferredSizeWidget? toolbar, - PreferredSizeWidget? temporaryToolbar, - }) = _ToolRuntimeState; - - MouseCursor get currentCursor => temporaryCursor ?? cursor; - - List getAllForegrounds([bool networking = true]) => [ - ...foregrounds, - ...?temporaryForegrounds, - ...toggleableForegrounds.values.expand((e) => e), - if (networking) ...networkingForegrounds, - ]; -} - -class ToolCubit extends Cubit { - ToolCubit([ToolRuntimeState? initial]) - : super(initial ?? ToolRuntimeState(handler: HandHandler())); - - final foregroundRefreshRunner = CoalescedAsyncRunner(delay: Duration.zero); - - void replace(ToolRuntimeState state) => emit(state); - - Handler getHandler({bool disableTemporary = false, bool editable = true}) { - if (!editable) return HandHandler(); - return disableTemporary - ? state.handler - : state.temporaryHandler ?? state.handler; - } - - T? fetchHandler({ - bool disableTemporary = false, - bool editable = true, - }) { - final handler = getHandler( - disableTemporary: disableTemporary, - editable: editable, - ); - if (handler is T) return handler; - return null; - } - - void setActiveTool({ - required int? index, - required Handler handler, - required MouseCursor cursor, - required List foregrounds, - required PreferredSizeWidget? toolbar, - required Map rendererStates, - }) => emit( - state.copyWith( - index: index, - handler: handler, - cursor: cursor, - foregrounds: foregrounds, - toolbar: toolbar, - temporaryForegrounds: null, - temporaryHandler: null, - temporaryToolbar: null, - temporaryCursor: null, - temporaryIndex: null, - ), - ); - - void setTemporaryTool({ - required Handler? handler, - required int? index, - required List? foregrounds, - required PreferredSizeWidget? toolbar, - required MouseCursor? cursor, - required Map? rendererStates, - TemporaryState? temporaryState, - }) => emit( - state.copyWith( - temporaryHandler: handler, - temporaryIndex: index, - temporaryForegrounds: foregrounds, - temporaryToolbar: toolbar, - temporaryCursor: cursor, - temporaryState: temporaryState ?? state.temporaryState, - ), - ); - - void setToggleable({ - Map>? handlers, - Map>? foregrounds, - }) => emit( - state.copyWith( - toggleableHandlers: handlers ?? state.toggleableHandlers, - toggleableForegrounds: foregrounds ?? state.toggleableForegrounds, + name: controller.networkingService.userName, ), ); - - void setForegrounds({ - List? foregrounds, - List? temporaryForegrounds, - Map>? toggleableForegrounds, - List? networkingForegrounds, - MouseCursor? cursor, - MouseCursor? temporaryCursor, - Map? rendererStates, - Map? temporaryRendererStates, - }) => emit( - state.copyWith( - foregrounds: foregrounds ?? state.foregrounds, - temporaryForegrounds: temporaryForegrounds ?? state.temporaryForegrounds, - toggleableForegrounds: - toggleableForegrounds ?? state.toggleableForegrounds, - networkingForegrounds: - networkingForegrounds ?? state.networkingForegrounds, - cursor: cursor ?? state.cursor, - temporaryCursor: temporaryCursor ?? state.temporaryCursor, - ), - ); - - void setToolbar({ - PreferredSizeWidget? toolbar, - PreferredSizeWidget? temporaryToolbar, - }) => emit( - state.copyWith( - toolbar: toolbar ?? state.toolbar, - temporaryToolbar: temporaryToolbar ?? state.temporaryToolbar, - ), - ); - - void setCursor(MouseCursor cursor) { - if (state.cursor != cursor) emit(state.copyWith(cursor: cursor)); - } - - void setIndex(int? index) => emit(state.copyWith(index: index)); - - void setSelection(Selection? selection) => - emit(state.copyWith(selection: selection)); - - void insertSelection(dynamic selected, [bool toggle = true]) { - final selection = state.selection; - if (selection == null) { - setSelection(Selection.from(selected)); - return; - } - Selection? next; - if (selection.selected.contains(selected) && toggle) { - if (selection.selected.length != 1) { - next = selection.remove(selected); - } - } else { - next = selection.insert(selected); - } - setSelection(next); - } - - void changeSelection(dynamic selected, [bool toggle = true]) { - Selection? selection; - if (selected is Selection?) { - selection = selected; - } else if (!toggle || - !(state.selection?.selected.contains(selected) ?? false)) { - selection = Selection.from(selected); - } - setSelection(selection); - } - - void removeSelection(List selected) { - Selection? selection = state.selection; - if (selection == null) return; - for (final s in selected) { - selection = selection?.remove(s); - } - setSelection(selection); - } - - void resetSelection({bool force = false}) { - if (force || !state.pinned) emit(state.copyWith(selection: null)); - } - - Tool? getTool(DocumentInfo info) { - final index = state.index; - if (index == null || - info.tools.isEmpty || - index < 0 || - index >= info.tools.length) { - return null; - } - return info.tools[index]; - } - - T? fetchTool(DocumentInfo info) { - final tool = getTool(info); - if (tool is T) return tool; - return null; - } - - void togglePin() => emit(state.copyWith(pinned: !state.pinned)); - - void setTemporaryState(TemporaryState temporaryState) => - emit(state.copyWith(temporaryState: temporaryState)); - - void resetRuntime() => emit(ToolRuntimeState(handler: HandHandler())); - - Future resetInput(dynamic bloc, EditorInputCubit inputCubit) async { - await state.handler.resetInput(bloc); - inputCubit.resetInputState(); - } - - void changeTemporaryHandlerMove(RendererCubit rendererCubit) { - setTemporaryTool( - handler: HandHandler(), - index: null, - foregrounds: null, - toolbar: null, - cursor: null, - rendererStates: null, - ); - rendererCubit.setRendererStates(temporaryRendererStates: const {}); - } - - Future updateHandler( - DocumentBloc bloc, - RendererCubit rendererCubit, - Handler handler, - ) async { - replace( - state.copyWith( - handler: handler, - cursor: handler.cursor ?? MouseCursor.defer, - toolbar: await handler.getToolbar(bloc), - ), - ); - rendererCubit.setRendererStates(rendererStates: handler.rendererStates); - } - - Future updateTool( - EditorController controller, - DocumentBloc bloc, - Tool tool, - ) async { - final docState = bloc.state; - if (docState is! DocumentLoadSuccess) return; - state.handler.dispose(bloc); - final handler = Handler.fromTool(tool); - for (final renderer in state.foregrounds) { - renderer.dispose(); - } - final foregrounds = handler.createForegrounds( - controller, - docState.data, - docState.page, - docState.info, - docState.currentArea, - ); - if (handler.setupForegrounds) { - await Future.wait( - foregrounds.map( - (e) async => await e.setup( - controller.transformCubit, - docState.data, - docState.assetService, - docState.page, - ), - ), - ); - } - setActiveTool( - index: state.index, - handler: handler, - cursor: handler.cursor ?? MouseCursor.defer, - foregrounds: foregrounds, - toolbar: await handler.getToolbar(bloc), - rendererStates: handler.rendererStates, - ); - controller.rendererCubit.setRendererStates( - rendererStates: handler.rendererStates, - ); - } - - Future updateTemporaryTool( - EditorController controller, - DocumentBloc bloc, - Tool tool, - ) async { - final docState = bloc.state; - if (docState is! DocumentLoadSuccess) return; - state.temporaryHandler?.dispose(bloc); - final handler = Handler.fromTool(tool); - for (final renderer in state.temporaryForegrounds ?? const []) { - renderer.dispose(); - } - final foregrounds = handler.createForegrounds( - controller, - docState.data, - docState.page, - docState.info, - docState.currentArea, - ); - if (handler.setupForegrounds) { - await Future.wait( - foregrounds.map( - (e) async => await e.setup( - controller.transformCubit, - docState.data, - docState.assetService, - docState.page, - ), - ), - ); - } - setTemporaryTool( - handler: handler, - index: state.temporaryIndex, - foregrounds: foregrounds, - toolbar: await handler.getToolbar(bloc), - cursor: handler.cursor, - rendererStates: handler.rendererStates, - ); - controller.rendererCubit.setRendererStates( - temporaryRendererStates: handler.rendererStates, - ); - } - - Future updateTogglingTools( - EditorController controller, - DocumentBloc bloc, - List tools, - ) async { - final blocState = bloc.state; - if (blocState is! DocumentLoadSuccess) return; - final newHandlers = Map>.from(state.toggleableHandlers); - final newForegrounds = Map>.from( - state.toggleableForegrounds, - ); - final currentTools = blocState.info.tools; - for (final tool in tools) { - if (tool.id == null) continue; - final index = currentTools.indexWhere((element) => element.id == tool.id); - if (index == -1) continue; - final old = state.toggleableHandlers[index]; - if (old == null || old.data == tool) continue; - old.dispose(bloc); - for (final renderer in state.toggleableForegrounds[index] ?? []) { - renderer.dispose(); - } - final handler = Handler.fromTool(tool); - final foregrounds = handler.createForegrounds( - controller, - blocState.data, - blocState.page, - blocState.info, - blocState.currentArea, - ); - if (handler.setupForegrounds) { - await Future.wait( - foregrounds.map( - (e) async => await e.setup( - controller.transformCubit, - blocState.data, - blocState.assetService, - blocState.page, - ), - ), - ); - } - newHandlers[index] = handler; - newForegrounds[index] = foregrounds; - } - setToggleable(handlers: newHandlers, foregrounds: newForegrounds); - } - - void disposeForegrounds() { - for (final r in state.foregrounds) { - r.dispose(); - } - } - - void disposeTemporaryForegrounds() { - for (final r in state.temporaryForegrounds ?? []) { - r.dispose(); - } - } - - void disposeNetworkingForegrounds() { - for (final r in state.networkingForegrounds) { - r.dispose(); - } - } - - void disposeToggleableForegrounds() { - for (final r in state.toggleableForegrounds.values.expand((e) => e)) { - r.dispose(); - } - } - - void disposeAllForegrounds() { - disposeForegrounds(); - disposeTemporaryForegrounds(); - disposeNetworkingForegrounds(); - disposeToggleableForegrounds(); - } - - R useHandler( - DocumentBloc bloc, - int index, - R Function(Handler handler) callback, { - required bool editable, - }) { - Handler? handler; - bool needsDispose = false; - if (state.index == index) { - handler = fetchHandler>( - disableTemporary: true, - editable: editable, - ); - } else if (state.toggleableHandlers.containsKey(index)) { - handler = state.toggleableHandlers[index]; - } - if (handler == null) { - List tools = const []; - final blocState = bloc.state; - if (blocState is DocumentLoaded) tools = blocState.info.tools; - final tool = tools.elementAtOrNull(index) ?? HandTool(); - handler = Handler.fromTool(tool); - needsDispose = true; - } - final result = callback(handler); - if (needsDispose) { - if (result is Future) { - result.then((value) => handler?.dispose(bloc)); - } else { - handler.dispose(bloc); - } - } - return result; - } - - Future changeTool( - EditorController controller, - DocumentBloc bloc, { - int? index, - BuildContext? context, - Handler? handler, - bool allowBake = true, - }) async { - talker.verbose('Changing tool to index: $index'); - await resetInput(bloc, controller.inputCubit); - final blocState = bloc.state; - if (blocState is! DocumentLoadSuccess) return null; - if (controller.saveCubit.state.embedding?.editable == false) { - return null; - } - final document = blocState.data; - final info = blocState.info; - index ??= state.index ?? 0; - if (handler == null && (index < 0 || index >= info.tools.length)) { - return null; - } - handler ??= Handler.fromTool(info.tools[index]); - var selectState = SelectState.normal; - if (context != null) { - selectState = await handler.onSelected(context); - } - if (selectState != SelectState.none) { - state.handler.dispose(bloc); - state.temporaryHandler?.dispose(bloc); - disposeTemporaryForegrounds(); - disposeForegrounds(); - final foregrounds = handler.createForegrounds( - controller, - document, - blocState.page, - info, - blocState.currentArea, - ); - if (handler.setupForegrounds) { - await Future.wait( - foregrounds.map( - (e) async => await e.setup( - controller.transformCubit, - document, - blocState.assetService, - blocState.page, - ), - ), - ); - } - if (selectState == SelectState.normal) { - controller.editorSessionCubit?.updateSelectedTool(handler.data, index); - setActiveTool( - index: index, - handler: handler, - cursor: handler.cursor ?? MouseCursor.defer, - foregrounds: foregrounds, - toolbar: await handler.getToolbar(bloc), - rendererStates: handler.rendererStates, - ); - controller.rendererCubit.setRendererStates( - rendererStates: handler.rendererStates, - temporaryRendererStates: const {}, - ); - if (allowBake) { - await controller.rendererCubit.bake(controller, blocState); - } - } else { - if (isHandlerEnabled(index)) { - disableHandler(bloc, index); - } else { - setToggleable( - handlers: {...state.toggleableHandlers, index: handler}, - foregrounds: {...state.toggleableForegrounds, index: foregrounds}, - ); - } - } - } - return handler; - } - - Future toggleHandler( - EditorController controller, - DocumentBloc bloc, - int index, - ) async { - if (state.toggleableHandlers.containsKey(index)) { - disableHandler(bloc, index); - } else { - await enableHandler(controller, bloc, index); - } - } - - Future enableHandler( - EditorController controller, - DocumentBloc bloc, - int index, - ) async { - final blocState = bloc.state; - if (blocState is! DocumentLoaded) return null; - if (index < 0 || index >= blocState.info.tools.length) { - return null; - } - final tool = blocState.info.tools[index]; - final handler = Handler.fromTool(tool); - final document = blocState.data; - final page = blocState.page; - final info = blocState.info; - final currentArea = blocState.currentArea; - final foregrounds = handler.createForegrounds( - controller, - document, - page, - info, - currentArea, - ); - if (handler.setupForegrounds) { - await Future.wait( - foregrounds.map( - (e) async => await e.setup( - controller.transformCubit, - document, - blocState.assetService, - page, - ), - ), - ); - } - setToggleable( - handlers: Map.from(state.toggleableHandlers)..[index] = handler, - foregrounds: Map.from(state.toggleableForegrounds)..[index] = foregrounds, - ); - return handler; - } - - bool disableHandler(DocumentBloc bloc, int index) { - final handler = state.toggleableHandlers[index]; - if (handler == null) { - return false; - } - handler.dispose(bloc); - final foregrounds = Map>.from( - state.toggleableForegrounds, - ); - final current = foregrounds.remove(index); - for (final r in current ?? []) { - r.dispose(); - } - setToggleable( - handlers: Map.from(state.toggleableHandlers)..remove(index), - foregrounds: foregrounds, - ); - return true; - } - - bool isHandlerEnabled(int index) => - state.toggleableHandlers.containsKey(index); - - void reset(EditorController controller, DocumentBloc bloc) { - for (final r in controller.rendererCubit.renderers) { - r.dispose(); - } - controller.rendererCubit.initializedElements.clear(); - state.handler.dispose(bloc); - state.temporaryHandler?.dispose(bloc); - for (var e in state.toggleableHandlers.values) { - e.dispose(bloc); - } - disposeAllForegrounds(); - resetRuntime(); - controller.rendererCubit.replace(const RendererRuntimeState()); - } - - Future changeTemporaryHandlerIndex( - BuildContext context, - EditorController controller, - int index, { - DocumentBloc? bloc, - TemporaryState temporaryState = TemporaryState.allowClick, - bool force = false, - }) async { - bloc ??= context.read(); - final blocState = bloc.state; - if (blocState is! DocumentLoadSuccess) return null; - if (index < 0 || index >= blocState.info.tools.length) { - return null; - } - final tool = blocState.info.tools[index]; - final temporaryHandler = state.temporaryHandler; - if (!force && index == state.temporaryIndex && temporaryHandler != null) { - return temporaryHandler; - } - return changeTemporaryHandler( - context, - controller, - tool, - bloc: bloc, - temporaryState: temporaryState, - index: index, - ); - } - - Future?> changeTemporaryHandler( - BuildContext context, - EditorController controller, - T tool, { - DocumentBloc? bloc, - int? index, - TemporaryState temporaryState = TemporaryState.allowClick, - }) async { - bloc ??= context.read(); - final handler = Handler.fromTool(tool); - final blocState = bloc.state; - if (blocState is! DocumentLoadSuccess) return null; - final document = blocState.data; - final page = blocState.page; - final currentArea = blocState.currentArea; - state.temporaryHandler?.dispose(bloc); - final selectState = await handler.onSelected(context); - - if (selectState == SelectState.normal) { - disposeTemporaryForegrounds(); - final temporaryForegrounds = handler.createForegrounds( - controller, - document, - page, - blocState.info, - currentArea, - ); - if (handler.setupForegrounds) { - await Future.wait( - temporaryForegrounds.map( - (e) async => await e.setup( - controller.transformCubit, - document, - blocState.assetService, - page, - ), - ), - ); - } - setTemporaryTool( - handler: handler, - index: index, - foregrounds: temporaryForegrounds, - toolbar: await handler.getToolbar(bloc), - cursor: handler.cursor, - rendererStates: handler.rendererStates, - temporaryState: temporaryState, - ); - controller.rendererCubit.setRendererStates( - temporaryRendererStates: handler.rendererStates, - ); - await controller.rendererCubit.bake(controller, blocState); - } else if (selectState == SelectState.toggle && index != null) { - await toggleHandler(controller, bloc, index); - } - return handler; - } - - void resetReleaseHandler(DocumentBloc bloc, [RendererCubit? rendererCubit]) { - if (state.temporaryState == TemporaryState.removeAfterRelease) { - resetTemporaryHandler(bloc, true, rendererCubit); - } - } - - void resetDownHandler(DocumentBloc bloc, [RendererCubit? rendererCubit]) { - resetTemporaryHandler(bloc, false, rendererCubit); - } - - void resetTemporaryHandler( - DocumentBloc bloc, [ - bool force = false, - RendererCubit? rendererCubit, - ]) { - if (state.temporaryHandler == null) { - return; - } - if (!force && state.temporaryState != TemporaryState.removeAfterClick) { - if (state.temporaryState == TemporaryState.allowClick) { - setTemporaryState(TemporaryState.removeAfterClick); - } - return; - } - state.temporaryHandler?.dispose(bloc); - disposeTemporaryForegrounds(); - setTemporaryTool( - handler: null, - index: null, - foregrounds: null, - toolbar: null, - cursor: null, - rendererStates: null, - ); - rendererCubit?.setRendererStates(temporaryRendererStates: const {}); - } - - Future refresh( - EditorController controller, - DocumentLoaded blocState, { - bool allowBake = true, - }) async { - talker.verbose('Refreshing tools'); - final document = blocState.data; - final page = blocState.page; - final info = blocState.info; - final assetService = blocState.assetService; - final currentArea = blocState.currentArea; - const mapEq = MapEquality(); - if (!controller.isClosed) { - disposeAllForegrounds(); - final temporaryForegrounds = state.temporaryHandler?.createForegrounds( - controller, - document, - page, - info, - currentArea, - ); - if (temporaryForegrounds != null && - state.temporaryHandler?.setupForegrounds == true) { - await Future.wait( - temporaryForegrounds.map( - (e) async => await e.setup( - controller.transformCubit, - document, - assetService, - page, - ), - ), - ); - } - final foregrounds = state.handler.createForegrounds( - controller, - document, - page, - info, - currentArea, - ); - if (state.handler.setupForegrounds) { - await Future.wait( - foregrounds.map( - (e) async => await e.setup( - controller.transformCubit, - document, - assetService, - page, - ), - ), - ); - } - final toggleableForegrounds = >{}; - for (final entry in state.toggleableHandlers.entries) { - final handler = entry.value; - final index = entry.key; - final foregrounds = handler.createForegrounds( - controller, - document, - page, - info, - currentArea, - ); - if (handler.setupForegrounds) { - await Future.wait( - foregrounds.map( - (e) async => await e.setup( - controller.transformCubit, - document, - assetService, - page, - ), - ), - ); - } - toggleableForegrounds[index] = foregrounds; - } - final rendererStates = state.handler.rendererStates; - final temporaryRendererStates = state.temporaryHandler?.rendererStates; - final statesChanged = !mapEq.equals( - controller.rendererCubit.state.rendererStates, - rendererStates, - ); - final temporaryStatesChanged = !mapEq.equals( - controller.rendererCubit.state.temporaryRendererStates, - temporaryRendererStates, - ); - final shouldBake = statesChanged || temporaryStatesChanged; - setForegrounds( - temporaryForegrounds: temporaryForegrounds, - toggleableForegrounds: toggleableForegrounds, - foregrounds: foregrounds, - cursor: state.handler.cursor ?? MouseCursor.defer, - temporaryCursor: state.temporaryHandler?.cursor, - ); - controller.rendererCubit.setRendererStates( - rendererStates: statesChanged - ? rendererStates - : controller.rendererCubit.state.rendererStates, - temporaryRendererStates: temporaryStatesChanged - ? temporaryRendererStates - : controller.rendererCubit.state.temporaryRendererStates, - ); - if (allowBake) { - if (shouldBake) { - return controller.rendererCubit.bake( - controller, - blocState, - reset: true, - ); - } else if (!controller.rendererCubit.state.cameraViewport.baked) { - return controller.rendererCubit.delayedBake(controller, blocState); - } - } - } - } - - Future refreshToolbar(DocumentBloc bloc) async { - final toolbar = await state.handler.getToolbar(bloc); - final temporaryToolbar = await state.temporaryHandler?.getToolbar(bloc); - setToolbar(toolbar: toolbar, temporaryToolbar: temporaryToolbar); - } - - Future refreshForegrounds( - EditorController controller, - DocumentLoaded blocState, - ) => foregroundRefreshRunner.schedule( - () => _refreshForegrounds(controller, blocState), - ); - - Future _refreshForegrounds( - EditorController controller, - DocumentLoaded blocState, - ) async { - if (controller.isClosed) return; - final document = blocState.data; - final page = blocState.page; - final info = blocState.info; - final assetService = blocState.assetService; - final currentArea = blocState.currentArea; - - disposeForegrounds(); - disposeTemporaryForegrounds(); - - final temporaryForegrounds = state.temporaryHandler?.createForegrounds( - controller, - document, - page, - info, - currentArea, - ); - if (temporaryForegrounds != null && - temporaryForegrounds.isNotEmpty && - state.temporaryHandler?.setupForegrounds == true) { - await Future.wait( - temporaryForegrounds.map( - (e) async => await e.setup( - controller.transformCubit, - document, - assetService, - page, - ), - ), - ); - } - - final foregrounds = state.handler.createForegrounds( - controller, - document, - page, - info, - currentArea, - ); - if (foregrounds.isNotEmpty && state.handler.setupForegrounds) { - await Future.wait( - foregrounds.map( - (e) async => await e.setup( - controller.transformCubit, - document, - assetService, - page, - ), - ), - ); - } - - const mapEq = MapEquality(); - final rendererStates = state.handler.rendererStates; - final temporaryRendererStates = state.temporaryHandler?.rendererStates; - final statesChanged = !mapEq.equals( - controller.rendererCubit.state.rendererStates, - rendererStates, - ); - final temporaryStatesChanged = !mapEq.equals( - controller.rendererCubit.state.temporaryRendererStates, - temporaryRendererStates, - ); - - setForegrounds( - foregrounds: foregrounds, - temporaryForegrounds: temporaryForegrounds, - cursor: state.handler.cursor ?? MouseCursor.defer, - temporaryCursor: state.temporaryHandler?.cursor, - ); - controller.rendererCubit.setRendererStates( - rendererStates: statesChanged - ? rendererStates - : controller.rendererCubit.state.rendererStates, - temporaryRendererStates: temporaryStatesChanged - ? temporaryRendererStates - : controller.rendererCubit.state.temporaryRendererStates, - ); - - if (statesChanged || temporaryStatesChanged) { - await controller.rendererCubit.bake(controller, blocState, reset: true); - } - } - - void updateIndex(EditorController controller, DocumentBloc bloc) { - final docState = bloc.state; - if (docState is! DocumentLoadSuccess) return; - final info = docState.info; - final index = info.tools.indexOf(state.handler.data); - if (index < 0) { - changeTool(controller, bloc, index: state.index ?? 0); - } - if (index == state.index) { - return; - } - setIndex(index); - final selection = state.selection; - if (selection?.selected.contains(state.handler.data) ?? false) { - resetSelection(); - } - } - - Future disposeRuntime(dynamic bloc) async { - state.handler.dispose(bloc); - state.temporaryHandler?.dispose(bloc); - for (final handler in state.toggleableHandlers.values) { - handler.dispose(bloc); - } - for (final renderer in state.getAllForegrounds()) { - renderer.dispose(); - } - foregroundRefreshRunner.cancel(); - await foregroundRefreshRunner.disposeAndWait(); - } -} - -@freezed -sealed class EditorInputState with _$EditorInputState { - const factory EditorInputState({ - Offset? lastPosition, - @Default([]) List pointers, - int? buttons, - @Default(false) bool penDetected, - @Default(false) bool sessionPenOnlyInput, - @Default(HideState.visible) HideState hideUi, - }) = _EditorInputState; -} - -class EditorInputCubit extends Cubit { - EditorInputCubit( - this.settingsCubit, [ - super.initial = const EditorInputState(), - ]); - - final SettingsCubit settingsCubit; - - void replace(EditorInputState state) => emit(state); - - void setPenDetected(bool detected, {bool enableSessionPenOnly = false}) { - if (state.penDetected == detected && - (!enableSessionPenOnly || state.sessionPenOnlyInput)) { - return; - } - emit( - state.copyWith( - penDetected: detected, - sessionPenOnlyInput: enableSessionPenOnly - ? true - : state.sessionPenOnlyInput, - ), - ); - } - - bool get effectivePenOnlyInput { - final setting = settingsCubit.state.penOnlyInput; - if (setting != null) return setting; - return state.sessionPenOnlyInput; - } - - bool get moveEnabled => - (settingsCubit.state.inputGestures && state.pointers.length > 1) && - settingsCubit.state.moveOnGesture; - - void detectPen(bool detected) { - if (state.penDetected == detected) return; - setPenDetected( - detected, - enableSessionPenOnly: - detected && - settingsCubit.state.penOnlyInput == null && - !state.sessionPenOnlyInput, - ); - } - - void setSessionPenOnlyInput(bool value) { - if (state.sessionPenOnlyInput != value) { - emit(state.copyWith(sessionPenOnlyInput: value)); - } - } - - void updateLastPosition(Offset position) { - final lastPos = state.lastPosition; - if (lastPos != null) { - final dx = (position.dx - lastPos.dx).abs(); - final dy = (position.dy - lastPos.dy).abs(); - if (dx < 1 && dy < 1) return; - } - emit(state.copyWith(lastPosition: position)); - } - - void addPointer(int pointer) { - if (!state.pointers.contains(pointer)) { - emit(state.copyWith(pointers: [...state.pointers, pointer])); - } - } - - void removePointer(int pointer) { - if (state.pointers.contains(pointer)) { - emit( - state.copyWith( - pointers: state.pointers.where((p) => p != pointer).toList(), - ), - ); - } - } - - void setButtons(int buttons) => emit(state.copyWith(buttons: buttons)); - - void removeButtons() => emit(state.copyWith(buttons: null)); - - void resetInputState() => emit(state.copyWith(buttons: null, pointers: [])); - - void toggleKeyboardHideUI() => emit( - state.copyWith( - hideUi: state.hideUi == HideState.visible - ? HideState.keyboard - : HideState.visible, - ), - ); - - void enterTouchHideUI() => emit(state.copyWith(hideUi: HideState.touch)); - - void exitHideUI() => emit(state.copyWith(hideUi: HideState.visible)); } -@freezed -sealed class DocumentSaveState with _$DocumentSaveState { - const DocumentSaveState._(); - - const factory DocumentSaveState({ - @Default(false) bool isSaveDelayed, - @Default(AssetLocation(path: '')) AssetLocation location, - Embedding? embedding, - @Default(SaveState.saved) SaveState saved, - @Default(false) bool isCreating, - }) = _DocumentSaveState; - - bool get absolute => saved == SaveState.absoluteRead; -} - -class DocumentSaveCubit extends Cubit { - DocumentSaveCubit( - this.settingsCubit, [ - super.initial = const DocumentSaveState(), - ]); - - final SettingsCubit settingsCubit; - - final savingLock = Lock(); - - void replace(DocumentSaveState state) => emit(state); - - void setSaveState({ - AssetLocation? location, - SaveState? saved, - bool absolute = false, - bool? isCreating, - bool keepRead = false, - }) => emit( - state.copyWith( - location: location ?? state.location, - isCreating: isCreating ?? state.isCreating, - saved: (absolute || (keepRead && state.absolute)) - ? SaveState.absoluteRead - : saved ?? state.saved, - ), - ); - - void setDelayed(bool delayed) => emit(state.copyWith(isSaveDelayed: delayed)); - - ExternalStorage? getRemoteStorage() => - settingsCubit.getRemote(state.location.remote); - - bool hasAutosave(NetworkingService networkingService) => - settingsCubit.state.autosave && - (networkingService.isActive || - !(state.embedding?.save ?? true) || - (!kIsWeb && - !state.absolute && - (state.location.isEmpty || - (state.location.fileType?.isNote() ?? false)) && - (state.location.remote.isEmpty || - (settingsCubit - .getRemote(state.location.remote) - ?.hasDocumentCached(state.location.path) ?? - false)))); - - Future save( - DocumentBloc bloc, - NetworkingService networkingService, { - AssetLocation? location, - bool force = false, - bool isAutosave = false, - }) async { - final absolute = state.absolute; - if (location == null && - !force && - (state.saved == SaveState.saved || - state.saved == SaveState.absoluteRead)) { - return state.location; - } - if (networkingService.isClient) { - return AssetLocation.empty; - } - if (state.isSaveDelayed && isAutosave) { - return state.location; - } - final storage = getRemoteStorage(); - final fileSystem = bloc.state.fileSystem.buildDocumentSystem(storage); - final isDelayed = settingsCubit.state.delayedAutosave; - if (isDelayed && isAutosave) { - final seconds = max(0, settingsCubit.state.autosaveDelaySeconds); - setDelayed(true); - await Future.delayed(Duration(seconds: seconds)); - if (!state.isSaveDelayed) { - return state.location; - } - } - return savingLock.synchronized(() async { - if (location == null && - !force && - (state.saved == SaveState.saved || - state.saved == SaveState.absoluteRead)) { - return state.location; - } - var current = location ?? state.location; - if (isClosed) { - return current; - } - setSaveState(saved: SaveState.saving, location: current); - setDelayed(false); - final blocState = bloc.state; - final currentData = await blocState.saveData(); - if (isClosed) { - return current; - } - if (currentData == null || state.embedding != null) { - setSaveState(saved: SaveState.saved); - return AssetLocation.empty; - } - if (absolute || !(current.fileType?.isNote() ?? false)) { - final file = await compute(_toFile, (currentData, false)); - final document = await fileSystem.createFileWithName( - name: currentData.name, - suffix: '.bfly', - directory: absolute - ? null - : current.fileExtension.isEmpty - ? state.location.path - : state.location.parent, - file, - ); - current = document.location; - } else { - final file = await compute(_toFile, ( - currentData, - current.fileType == AssetFileType.textNote, - )); - await fileSystem.updateFile(current.path, file); - } - settingsCubit.addRecentHistory(current); - if (isClosed) { - return current; - } - setSaveState( - saved: state.saved == SaveState.saving ? SaveState.saved : state.saved, - location: current, - ); - return current; - }); - } -} - -@freezed -sealed class EditorViewState with _$EditorViewState { - const factory EditorViewState({ - @Default(UtilitiesState()) UtilitiesState utilities, - @Default(ViewOption()) ViewOption viewOption, - @Default(true) bool areaNavigatorCreate, - @Default(true) bool areaNavigatorExact, - @Default(false) bool areaNavigatorAsk, - @Default(false) bool navigatorEnabled, - @Default(NavigatorPage.waypoints) NavigatorPage navigatorPage, - @Default('') String userName, - }) = _EditorViewState; -} - -class EditorViewCubit extends Cubit { - EditorViewCubit({this.editorSessionCubit, EditorViewState? initial}) - : super(initial ?? const EditorViewState()); - - final EditorSessionCubit? editorSessionCubit; - - void replace(EditorViewState state) => emit(state); +enum SaveState { saved, saving, unsaved, absoluteRead } - void updateUtilities({UtilitiesState? utilities, ViewOption? view}) { - emit( - state.copyWith( - utilities: utilities ?? state.utilities, - viewOption: view ?? state.viewOption, - ), - ); - if (utilities != null) { - editorSessionCubit?.updateUtilities(utilities); - } - } +enum HideState { visible, keyboard, touch } - void setAreaNavigator({bool? create, bool? exact, bool? ask}) { - emit( - state.copyWith( - areaNavigatorCreate: create ?? state.areaNavigatorCreate, - areaNavigatorExact: exact ?? state.areaNavigatorExact, - areaNavigatorAsk: ask ?? state.areaNavigatorAsk, - ), - ); - editorSessionCubit?.updateAreaNavigator( - create: create, - exact: exact, - ask: ask, - ); - } +enum RendererState { visible, temporary, hidden } - void setNavigator({bool? enabled, NavigatorPage? page}) { - emit( - state.copyWith( - navigatorEnabled: enabled ?? state.navigatorEnabled, - navigatorPage: page ?? state.navigatorPage, - ), - ); - editorSessionCubit?.updateNavigator(enabled: enabled, page: page); - } +enum TemporaryState { allowClick, removeAfterClick, removeAfterRelease } - void setUserName(String name) => emit(state.copyWith(userName: name)); +abstract interface class EditorRuntimeContext { + SettingsCubit get settingsCubit; + RendererCubit get rendererCubit; + EditorInputCubit get inputCubit; + EditorViewCubit get viewCubit; } diff --git a/app/lib/cubits/editor_runtime.freezed.dart b/app/lib/cubits/editor_runtime.freezed.dart index 8a984670b232..586926c2c032 100644 --- a/app/lib/cubits/editor_runtime.freezed.dart +++ b/app/lib/cubits/editor_runtime.freezed.dart @@ -12,7 +12,7 @@ part of 'editor_runtime.dart'; // dart format off T _$identity(T value) => value; /// @nodoc -mixin _$RendererRuntimeState { +mixin _$RendererRuntimeState implements DiagnosticableTreeMixin { CameraViewport get cameraViewport; Map get rendererStates; Map? get temporaryRendererStates; /// Create a copy of RendererRuntimeState @@ -22,11 +22,17 @@ mixin _$RendererRuntimeState { $RendererRuntimeStateCopyWith get copyWith => _$RendererRuntimeStateCopyWithImpl(this as RendererRuntimeState, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'RendererRuntimeState')) + ..add(DiagnosticsProperty('cameraViewport', cameraViewport))..add(DiagnosticsProperty('rendererStates', rendererStates))..add(DiagnosticsProperty('temporaryRendererStates', temporaryRendererStates)); +} @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'RendererRuntimeState(cameraViewport: $cameraViewport, rendererStates: $rendererStates, temporaryRendererStates: $temporaryRendererStates)'; } @@ -80,7 +86,7 @@ $CameraViewportCopyWith<$Res> get cameraViewport { /// @nodoc -class _RendererRuntimeState extends RendererRuntimeState { +class _RendererRuntimeState extends RendererRuntimeState with DiagnosticableTreeMixin { const _RendererRuntimeState({this.cameraViewport = const CameraViewport.unbaked(), final Map rendererStates = const {}, final Map? temporaryRendererStates = const {}}): _rendererStates = rendererStates,_temporaryRendererStates = temporaryRendererStates,super._(); @@ -109,11 +115,17 @@ class _RendererRuntimeState extends RendererRuntimeState { _$RendererRuntimeStateCopyWith<_RendererRuntimeState> get copyWith => __$RendererRuntimeStateCopyWithImpl<_RendererRuntimeState>(this, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'RendererRuntimeState')) + ..add(DiagnosticsProperty('cameraViewport', cameraViewport))..add(DiagnosticsProperty('rendererStates', rendererStates))..add(DiagnosticsProperty('temporaryRendererStates', temporaryRendererStates)); +} @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'RendererRuntimeState(cameraViewport: $cameraViewport, rendererStates: $rendererStates, temporaryRendererStates: $temporaryRendererStates)'; } @@ -164,7 +176,7 @@ $CameraViewportCopyWith<$Res> get cameraViewport { } /// @nodoc -mixin _$ToolRuntimeState { +mixin _$ToolRuntimeState implements DiagnosticableTreeMixin { int? get index; Handler get handler; Handler? get temporaryHandler; int? get temporaryIndex; List get foregrounds; Selection? get selection; bool get pinned; List? get temporaryForegrounds; Map> get toggleableHandlers; List get networkingForegrounds; Map> get toggleableForegrounds; MouseCursor get cursor; MouseCursor? get temporaryCursor; TemporaryState get temporaryState; PreferredSizeWidget? get toolbar; PreferredSizeWidget? get temporaryToolbar; /// Create a copy of ToolRuntimeState @@ -174,11 +186,17 @@ mixin _$ToolRuntimeState { $ToolRuntimeStateCopyWith get copyWith => _$ToolRuntimeStateCopyWithImpl(this as ToolRuntimeState, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ToolRuntimeState')) + ..add(DiagnosticsProperty('index', index))..add(DiagnosticsProperty('handler', handler))..add(DiagnosticsProperty('temporaryHandler', temporaryHandler))..add(DiagnosticsProperty('temporaryIndex', temporaryIndex))..add(DiagnosticsProperty('foregrounds', foregrounds))..add(DiagnosticsProperty('selection', selection))..add(DiagnosticsProperty('pinned', pinned))..add(DiagnosticsProperty('temporaryForegrounds', temporaryForegrounds))..add(DiagnosticsProperty('toggleableHandlers', toggleableHandlers))..add(DiagnosticsProperty('networkingForegrounds', networkingForegrounds))..add(DiagnosticsProperty('toggleableForegrounds', toggleableForegrounds))..add(DiagnosticsProperty('cursor', cursor))..add(DiagnosticsProperty('temporaryCursor', temporaryCursor))..add(DiagnosticsProperty('temporaryState', temporaryState))..add(DiagnosticsProperty('toolbar', toolbar))..add(DiagnosticsProperty('temporaryToolbar', temporaryToolbar)); +} @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'ToolRuntimeState(index: $index, handler: $handler, temporaryHandler: $temporaryHandler, temporaryIndex: $temporaryIndex, foregrounds: $foregrounds, selection: $selection, pinned: $pinned, temporaryForegrounds: $temporaryForegrounds, toggleableHandlers: $toggleableHandlers, networkingForegrounds: $networkingForegrounds, toggleableForegrounds: $toggleableForegrounds, cursor: $cursor, temporaryCursor: $temporaryCursor, temporaryState: $temporaryState, toolbar: $toolbar, temporaryToolbar: $temporaryToolbar)'; } @@ -236,7 +254,7 @@ as PreferredSizeWidget?, /// @nodoc -class _ToolRuntimeState extends ToolRuntimeState { +class _ToolRuntimeState extends ToolRuntimeState with DiagnosticableTreeMixin { const _ToolRuntimeState({this.index, required this.handler, this.temporaryHandler, this.temporaryIndex, final List foregrounds = const [], this.selection, this.pinned = false, final List? temporaryForegrounds, final Map> toggleableHandlers = const {}, final List networkingForegrounds = const [], final Map> toggleableForegrounds = const {}, this.cursor = MouseCursor.defer, this.temporaryCursor, this.temporaryState = TemporaryState.allowClick, this.toolbar, this.temporaryToolbar}): _foregrounds = foregrounds,_temporaryForegrounds = temporaryForegrounds,_toggleableHandlers = toggleableHandlers,_networkingForegrounds = networkingForegrounds,_toggleableForegrounds = toggleableForegrounds,super._(); @@ -296,11 +314,17 @@ class _ToolRuntimeState extends ToolRuntimeState { _$ToolRuntimeStateCopyWith<_ToolRuntimeState> get copyWith => __$ToolRuntimeStateCopyWithImpl<_ToolRuntimeState>(this, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ToolRuntimeState')) + ..add(DiagnosticsProperty('index', index))..add(DiagnosticsProperty('handler', handler))..add(DiagnosticsProperty('temporaryHandler', temporaryHandler))..add(DiagnosticsProperty('temporaryIndex', temporaryIndex))..add(DiagnosticsProperty('foregrounds', foregrounds))..add(DiagnosticsProperty('selection', selection))..add(DiagnosticsProperty('pinned', pinned))..add(DiagnosticsProperty('temporaryForegrounds', temporaryForegrounds))..add(DiagnosticsProperty('toggleableHandlers', toggleableHandlers))..add(DiagnosticsProperty('networkingForegrounds', networkingForegrounds))..add(DiagnosticsProperty('toggleableForegrounds', toggleableForegrounds))..add(DiagnosticsProperty('cursor', cursor))..add(DiagnosticsProperty('temporaryCursor', temporaryCursor))..add(DiagnosticsProperty('temporaryState', temporaryState))..add(DiagnosticsProperty('toolbar', toolbar))..add(DiagnosticsProperty('temporaryToolbar', temporaryToolbar)); +} @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'ToolRuntimeState(index: $index, handler: $handler, temporaryHandler: $temporaryHandler, temporaryIndex: $temporaryIndex, foregrounds: $foregrounds, selection: $selection, pinned: $pinned, temporaryForegrounds: $temporaryForegrounds, toggleableHandlers: $toggleableHandlers, networkingForegrounds: $networkingForegrounds, toggleableForegrounds: $toggleableForegrounds, cursor: $cursor, temporaryCursor: $temporaryCursor, temporaryState: $temporaryState, toolbar: $toolbar, temporaryToolbar: $temporaryToolbar)'; } @@ -355,7 +379,7 @@ as PreferredSizeWidget?, } /// @nodoc -mixin _$EditorInputState { +mixin _$EditorInputState implements DiagnosticableTreeMixin { Offset? get lastPosition; List get pointers; int? get buttons; bool get penDetected; bool get sessionPenOnlyInput; HideState get hideUi; /// Create a copy of EditorInputState @@ -365,6 +389,12 @@ mixin _$EditorInputState { $EditorInputStateCopyWith get copyWith => _$EditorInputStateCopyWithImpl(this as EditorInputState, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'EditorInputState')) + ..add(DiagnosticsProperty('lastPosition', lastPosition))..add(DiagnosticsProperty('pointers', pointers))..add(DiagnosticsProperty('buttons', buttons))..add(DiagnosticsProperty('penDetected', penDetected))..add(DiagnosticsProperty('sessionPenOnlyInput', sessionPenOnlyInput))..add(DiagnosticsProperty('hideUi', hideUi)); +} @override bool operator ==(Object other) { @@ -376,7 +406,7 @@ bool operator ==(Object other) { int get hashCode => Object.hash(runtimeType,lastPosition,const DeepCollectionEquality().hash(pointers),buttons,penDetected,sessionPenOnlyInput,hideUi); @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'EditorInputState(lastPosition: $lastPosition, pointers: $pointers, buttons: $buttons, penDetected: $penDetected, sessionPenOnlyInput: $sessionPenOnlyInput, hideUi: $hideUi)'; } @@ -424,7 +454,7 @@ as HideState, /// @nodoc -class _EditorInputState implements EditorInputState { +class _EditorInputState with DiagnosticableTreeMixin implements EditorInputState { const _EditorInputState({this.lastPosition, final List pointers = const [], this.buttons, this.penDetected = false, this.sessionPenOnlyInput = false, this.hideUi = HideState.visible}): _pointers = pointers; @@ -448,6 +478,12 @@ class _EditorInputState implements EditorInputState { _$EditorInputStateCopyWith<_EditorInputState> get copyWith => __$EditorInputStateCopyWithImpl<_EditorInputState>(this, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'EditorInputState')) + ..add(DiagnosticsProperty('lastPosition', lastPosition))..add(DiagnosticsProperty('pointers', pointers))..add(DiagnosticsProperty('buttons', buttons))..add(DiagnosticsProperty('penDetected', penDetected))..add(DiagnosticsProperty('sessionPenOnlyInput', sessionPenOnlyInput))..add(DiagnosticsProperty('hideUi', hideUi)); +} @override bool operator ==(Object other) { @@ -459,7 +495,7 @@ bool operator ==(Object other) { int get hashCode => Object.hash(runtimeType,lastPosition,const DeepCollectionEquality().hash(_pointers),buttons,penDetected,sessionPenOnlyInput,hideUi); @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'EditorInputState(lastPosition: $lastPosition, pointers: $pointers, buttons: $buttons, penDetected: $penDetected, sessionPenOnlyInput: $sessionPenOnlyInput, hideUi: $hideUi)'; } @@ -504,7 +540,7 @@ as HideState, } /// @nodoc -mixin _$DocumentSaveState { +mixin _$DocumentSaveState implements DiagnosticableTreeMixin { bool get isSaveDelayed; AssetLocation get location; Embedding? get embedding; SaveState get saved; bool get isCreating; /// Create a copy of DocumentSaveState @@ -514,6 +550,12 @@ mixin _$DocumentSaveState { $DocumentSaveStateCopyWith get copyWith => _$DocumentSaveStateCopyWithImpl(this as DocumentSaveState, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'DocumentSaveState')) + ..add(DiagnosticsProperty('isSaveDelayed', isSaveDelayed))..add(DiagnosticsProperty('location', location))..add(DiagnosticsProperty('embedding', embedding))..add(DiagnosticsProperty('saved', saved))..add(DiagnosticsProperty('isCreating', isCreating)); +} @override bool operator ==(Object other) { @@ -525,7 +567,7 @@ bool operator ==(Object other) { int get hashCode => Object.hash(runtimeType,isSaveDelayed,location,embedding,saved,isCreating); @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'DocumentSaveState(isSaveDelayed: $isSaveDelayed, location: $location, embedding: $embedding, saved: $saved, isCreating: $isCreating)'; } @@ -572,7 +614,7 @@ as bool, /// @nodoc -class _DocumentSaveState extends DocumentSaveState { +class _DocumentSaveState extends DocumentSaveState with DiagnosticableTreeMixin { const _DocumentSaveState({this.isSaveDelayed = false, this.location = const AssetLocation(path: ''), this.embedding, this.saved = SaveState.saved, this.isCreating = false}): super._(); @@ -589,6 +631,12 @@ class _DocumentSaveState extends DocumentSaveState { _$DocumentSaveStateCopyWith<_DocumentSaveState> get copyWith => __$DocumentSaveStateCopyWithImpl<_DocumentSaveState>(this, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'DocumentSaveState')) + ..add(DiagnosticsProperty('isSaveDelayed', isSaveDelayed))..add(DiagnosticsProperty('location', location))..add(DiagnosticsProperty('embedding', embedding))..add(DiagnosticsProperty('saved', saved))..add(DiagnosticsProperty('isCreating', isCreating)); +} @override bool operator ==(Object other) { @@ -600,7 +648,7 @@ bool operator ==(Object other) { int get hashCode => Object.hash(runtimeType,isSaveDelayed,location,embedding,saved,isCreating); @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'DocumentSaveState(isSaveDelayed: $isSaveDelayed, location: $location, embedding: $embedding, saved: $saved, isCreating: $isCreating)'; } @@ -644,7 +692,7 @@ as bool, } /// @nodoc -mixin _$EditorViewState { +mixin _$EditorViewState implements DiagnosticableTreeMixin { UtilitiesState get utilities; ViewOption get viewOption; bool get areaNavigatorCreate; bool get areaNavigatorExact; bool get areaNavigatorAsk; bool get navigatorEnabled; NavigatorPage get navigatorPage; String get userName; /// Create a copy of EditorViewState @@ -654,6 +702,12 @@ mixin _$EditorViewState { $EditorViewStateCopyWith get copyWith => _$EditorViewStateCopyWithImpl(this as EditorViewState, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'EditorViewState')) + ..add(DiagnosticsProperty('utilities', utilities))..add(DiagnosticsProperty('viewOption', viewOption))..add(DiagnosticsProperty('areaNavigatorCreate', areaNavigatorCreate))..add(DiagnosticsProperty('areaNavigatorExact', areaNavigatorExact))..add(DiagnosticsProperty('areaNavigatorAsk', areaNavigatorAsk))..add(DiagnosticsProperty('navigatorEnabled', navigatorEnabled))..add(DiagnosticsProperty('navigatorPage', navigatorPage))..add(DiagnosticsProperty('userName', userName)); +} @override bool operator ==(Object other) { @@ -665,7 +719,7 @@ bool operator ==(Object other) { int get hashCode => Object.hash(runtimeType,utilities,viewOption,areaNavigatorCreate,areaNavigatorExact,areaNavigatorAsk,navigatorEnabled,navigatorPage,userName); @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'EditorViewState(utilities: $utilities, viewOption: $viewOption, areaNavigatorCreate: $areaNavigatorCreate, areaNavigatorExact: $areaNavigatorExact, areaNavigatorAsk: $areaNavigatorAsk, navigatorEnabled: $navigatorEnabled, navigatorPage: $navigatorPage, userName: $userName)'; } @@ -733,7 +787,7 @@ $ViewOptionCopyWith<$Res> get viewOption { /// @nodoc -class _EditorViewState implements EditorViewState { +class _EditorViewState with DiagnosticableTreeMixin implements EditorViewState { const _EditorViewState({this.utilities = const UtilitiesState(), this.viewOption = const ViewOption(), this.areaNavigatorCreate = true, this.areaNavigatorExact = true, this.areaNavigatorAsk = false, this.navigatorEnabled = false, this.navigatorPage = NavigatorPage.waypoints, this.userName = ''}); @@ -753,6 +807,12 @@ class _EditorViewState implements EditorViewState { _$EditorViewStateCopyWith<_EditorViewState> get copyWith => __$EditorViewStateCopyWithImpl<_EditorViewState>(this, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'EditorViewState')) + ..add(DiagnosticsProperty('utilities', utilities))..add(DiagnosticsProperty('viewOption', viewOption))..add(DiagnosticsProperty('areaNavigatorCreate', areaNavigatorCreate))..add(DiagnosticsProperty('areaNavigatorExact', areaNavigatorExact))..add(DiagnosticsProperty('areaNavigatorAsk', areaNavigatorAsk))..add(DiagnosticsProperty('navigatorEnabled', navigatorEnabled))..add(DiagnosticsProperty('navigatorPage', navigatorPage))..add(DiagnosticsProperty('userName', userName)); +} @override bool operator ==(Object other) { @@ -764,7 +824,7 @@ bool operator ==(Object other) { int get hashCode => Object.hash(runtimeType,utilities,viewOption,areaNavigatorCreate,areaNavigatorExact,areaNavigatorAsk,navigatorEnabled,navigatorPage,userName); @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'EditorViewState(utilities: $utilities, viewOption: $viewOption, areaNavigatorCreate: $areaNavigatorCreate, areaNavigatorExact: $areaNavigatorExact, areaNavigatorAsk: $areaNavigatorAsk, navigatorEnabled: $navigatorEnabled, navigatorPage: $navigatorPage, userName: $userName)'; } diff --git a/app/lib/cubits/editor_tool.dart b/app/lib/cubits/editor_tool.dart new file mode 100644 index 000000000000..f3a8dd10c7cf --- /dev/null +++ b/app/lib/cubits/editor_tool.dart @@ -0,0 +1,1045 @@ +part of 'editor_runtime.dart'; + +@Freezed(equal: false) +sealed class ToolRuntimeState with _$ToolRuntimeState { + const ToolRuntimeState._(); + + const factory ToolRuntimeState({ + int? index, + required Handler handler, + Handler? temporaryHandler, + int? temporaryIndex, + @Default([]) List foregrounds, + Selection? selection, + @Default(false) bool pinned, + List? temporaryForegrounds, + @Default({}) Map> toggleableHandlers, + @Default([]) List networkingForegrounds, + @Default({}) Map> toggleableForegrounds, + @Default(MouseCursor.defer) MouseCursor cursor, + MouseCursor? temporaryCursor, + @Default(TemporaryState.allowClick) TemporaryState temporaryState, + PreferredSizeWidget? toolbar, + PreferredSizeWidget? temporaryToolbar, + }) = _ToolRuntimeState; + + MouseCursor get currentCursor => temporaryCursor ?? cursor; + + List getAllForegrounds([bool networking = true]) => [ + ...foregrounds, + ...?temporaryForegrounds, + ...toggleableForegrounds.values.expand((e) => e), + if (networking) ...networkingForegrounds, + ]; +} + +class ToolCubit extends Cubit { + ToolCubit([ToolRuntimeState? initial]) + : super(initial ?? ToolRuntimeState(handler: HandHandler())); + + final foregroundRefreshRunner = CoalescedAsyncRunner(delay: Duration.zero); + EditorController? _controller; + Timer? _networkingDebounceTimer; + + void bindController(EditorController controller) { + _controller = controller; + } + + @override + void onChange(Change change) { + super.onChange(change); + if (change.nextState.foregrounds != change.currentState.foregrounds || + change.nextState.temporaryForegrounds != + change.currentState.temporaryForegrounds) { + scheduleNetworkingState(); + } + } + + void scheduleNetworkingState({ + List>? foregrounds, + Offset? cursor, + }) { + final controller = _controller; + if (controller == null || controller.isClosed) return; + _networkingDebounceTimer?.cancel(); + _networkingDebounceTimer = Timer(const Duration(milliseconds: 50), () { + if (!controller.isClosed) { + _sendNetworkingState( + controller, + foregrounds: foregrounds, + cursor: cursor, + ); + } + }); + } + + void replace(ToolRuntimeState state) => emit(state); + + Handler getHandler({bool disableTemporary = false, bool editable = true}) { + if (!editable) return HandHandler(); + return disableTemporary + ? state.handler + : state.temporaryHandler ?? state.handler; + } + + T? fetchHandler({ + bool disableTemporary = false, + bool editable = true, + }) { + final handler = getHandler( + disableTemporary: disableTemporary, + editable: editable, + ); + if (handler is T) return handler; + return null; + } + + void setActiveTool({ + required int? index, + required Handler handler, + required MouseCursor cursor, + required List foregrounds, + required PreferredSizeWidget? toolbar, + required Map rendererStates, + }) => emit( + state.copyWith( + index: index, + handler: handler, + cursor: cursor, + foregrounds: foregrounds, + toolbar: toolbar, + temporaryForegrounds: null, + temporaryHandler: null, + temporaryToolbar: null, + temporaryCursor: null, + temporaryIndex: null, + ), + ); + + void setTemporaryTool({ + required Handler? handler, + required int? index, + required List? foregrounds, + required PreferredSizeWidget? toolbar, + required MouseCursor? cursor, + required Map? rendererStates, + TemporaryState? temporaryState, + }) => emit( + state.copyWith( + temporaryHandler: handler, + temporaryIndex: index, + temporaryForegrounds: foregrounds, + temporaryToolbar: toolbar, + temporaryCursor: cursor, + temporaryState: temporaryState ?? state.temporaryState, + ), + ); + + void setToggleable({ + Map>? handlers, + Map>? foregrounds, + }) => emit( + state.copyWith( + toggleableHandlers: handlers ?? state.toggleableHandlers, + toggleableForegrounds: foregrounds ?? state.toggleableForegrounds, + ), + ); + + void setForegrounds({ + List? foregrounds, + List? temporaryForegrounds, + Map>? toggleableForegrounds, + List? networkingForegrounds, + MouseCursor? cursor, + MouseCursor? temporaryCursor, + Map? rendererStates, + Map? temporaryRendererStates, + }) => emit( + state.copyWith( + foregrounds: foregrounds ?? state.foregrounds, + temporaryForegrounds: temporaryForegrounds ?? state.temporaryForegrounds, + toggleableForegrounds: + toggleableForegrounds ?? state.toggleableForegrounds, + networkingForegrounds: + networkingForegrounds ?? state.networkingForegrounds, + cursor: cursor ?? state.cursor, + temporaryCursor: temporaryCursor ?? state.temporaryCursor, + ), + ); + + void setToolbar({ + PreferredSizeWidget? toolbar, + PreferredSizeWidget? temporaryToolbar, + }) => emit( + state.copyWith( + toolbar: toolbar ?? state.toolbar, + temporaryToolbar: temporaryToolbar ?? state.temporaryToolbar, + ), + ); + + void setCursor(MouseCursor cursor) { + if (state.cursor != cursor) emit(state.copyWith(cursor: cursor)); + } + + void setIndex(int? index) => emit(state.copyWith(index: index)); + + void setSelection(Selection? selection) => + emit(state.copyWith(selection: selection)); + + void insertSelection(dynamic selected, [bool toggle = true]) { + final selection = state.selection; + if (selection == null) { + setSelection(Selection.from(selected)); + return; + } + Selection? next; + if (selection.selected.contains(selected) && toggle) { + if (selection.selected.length != 1) { + next = selection.remove(selected); + } + } else { + next = selection.insert(selected); + } + setSelection(next); + } + + void changeSelection(dynamic selected, [bool toggle = true]) { + Selection? selection; + if (selected is Selection?) { + selection = selected; + } else if (!toggle || + !(state.selection?.selected.contains(selected) ?? false)) { + selection = Selection.from(selected); + } + setSelection(selection); + } + + void removeSelection(List selected) { + Selection? selection = state.selection; + if (selection == null) return; + for (final s in selected) { + selection = selection?.remove(s); + } + setSelection(selection); + } + + void resetSelection({bool force = false}) { + if (force || !state.pinned) emit(state.copyWith(selection: null)); + } + + Tool? getTool(DocumentInfo info) { + final index = state.index; + if (index == null || + info.tools.isEmpty || + index < 0 || + index >= info.tools.length) { + return null; + } + return info.tools[index]; + } + + T? fetchTool(DocumentInfo info) { + final tool = getTool(info); + if (tool is T) return tool; + return null; + } + + void togglePin() => emit(state.copyWith(pinned: !state.pinned)); + + void setTemporaryState(TemporaryState temporaryState) => + emit(state.copyWith(temporaryState: temporaryState)); + + void resetRuntime() => emit(ToolRuntimeState(handler: HandHandler())); + + Future resetInput(dynamic bloc, EditorInputCubit inputCubit) async { + await state.handler.resetInput(bloc); + inputCubit.resetInputState(); + } + + void changeTemporaryHandlerMove(RendererCubit rendererCubit) { + setTemporaryTool( + handler: HandHandler(), + index: null, + foregrounds: null, + toolbar: null, + cursor: null, + rendererStates: null, + ); + rendererCubit.setRendererStates(temporaryRendererStates: const {}); + } + + Future updateHandler( + DocumentBloc bloc, + RendererCubit rendererCubit, + Handler handler, + ) async { + replace( + state.copyWith( + handler: handler, + cursor: handler.cursor ?? MouseCursor.defer, + toolbar: await handler.getToolbar(bloc), + ), + ); + rendererCubit.setRendererStates(rendererStates: handler.rendererStates); + } + + Future updateTool( + EditorController controller, + DocumentBloc bloc, + Tool tool, + ) async { + final docState = bloc.state; + if (docState is! DocumentLoadSuccess) return; + state.handler.dispose(bloc); + final handler = Handler.fromTool(tool); + for (final renderer in state.foregrounds) { + renderer.dispose(); + } + final foregrounds = handler.createForegrounds( + controller, + docState.data, + docState.page, + docState.info, + docState.currentArea, + ); + if (handler.setupForegrounds) { + await Future.wait( + foregrounds.map( + (e) async => await e.setup( + controller.transformCubit, + docState.data, + docState.assetService, + docState.page, + ), + ), + ); + } + setActiveTool( + index: state.index, + handler: handler, + cursor: handler.cursor ?? MouseCursor.defer, + foregrounds: foregrounds, + toolbar: await handler.getToolbar(bloc), + rendererStates: handler.rendererStates, + ); + controller.rendererCubit.setRendererStates( + rendererStates: handler.rendererStates, + ); + } + + Future updateTemporaryTool( + EditorController controller, + DocumentBloc bloc, + Tool tool, + ) async { + final docState = bloc.state; + if (docState is! DocumentLoadSuccess) return; + state.temporaryHandler?.dispose(bloc); + final handler = Handler.fromTool(tool); + for (final renderer in state.temporaryForegrounds ?? const []) { + renderer.dispose(); + } + final foregrounds = handler.createForegrounds( + controller, + docState.data, + docState.page, + docState.info, + docState.currentArea, + ); + if (handler.setupForegrounds) { + await Future.wait( + foregrounds.map( + (e) async => await e.setup( + controller.transformCubit, + docState.data, + docState.assetService, + docState.page, + ), + ), + ); + } + setTemporaryTool( + handler: handler, + index: state.temporaryIndex, + foregrounds: foregrounds, + toolbar: await handler.getToolbar(bloc), + cursor: handler.cursor, + rendererStates: handler.rendererStates, + ); + controller.rendererCubit.setRendererStates( + temporaryRendererStates: handler.rendererStates, + ); + } + + Future updateTogglingTools( + EditorController controller, + DocumentBloc bloc, + List tools, + ) async { + final blocState = bloc.state; + if (blocState is! DocumentLoadSuccess) return; + final newHandlers = Map>.from(state.toggleableHandlers); + final newForegrounds = Map>.from( + state.toggleableForegrounds, + ); + final currentTools = blocState.info.tools; + for (final tool in tools) { + if (tool.id == null) continue; + final index = currentTools.indexWhere((element) => element.id == tool.id); + if (index == -1) continue; + final old = state.toggleableHandlers[index]; + if (old == null || old.data == tool) continue; + old.dispose(bloc); + for (final renderer in state.toggleableForegrounds[index] ?? []) { + renderer.dispose(); + } + final handler = Handler.fromTool(tool); + final foregrounds = handler.createForegrounds( + controller, + blocState.data, + blocState.page, + blocState.info, + blocState.currentArea, + ); + if (handler.setupForegrounds) { + await Future.wait( + foregrounds.map( + (e) async => await e.setup( + controller.transformCubit, + blocState.data, + blocState.assetService, + blocState.page, + ), + ), + ); + } + newHandlers[index] = handler; + newForegrounds[index] = foregrounds; + } + setToggleable(handlers: newHandlers, foregrounds: newForegrounds); + } + + void disposeForegrounds() { + for (final r in state.foregrounds) { + r.dispose(); + } + } + + void disposeTemporaryForegrounds() { + for (final r in state.temporaryForegrounds ?? []) { + r.dispose(); + } + } + + void disposeNetworkingForegrounds() { + for (final r in state.networkingForegrounds) { + r.dispose(); + } + } + + void disposeToggleableForegrounds() { + for (final r in state.toggleableForegrounds.values.expand((e) => e)) { + r.dispose(); + } + } + + void disposeAllForegrounds() { + disposeForegrounds(); + disposeTemporaryForegrounds(); + disposeNetworkingForegrounds(); + disposeToggleableForegrounds(); + } + + R useHandler( + DocumentBloc bloc, + int index, + R Function(Handler handler) callback, { + required bool editable, + }) { + Handler? handler; + bool needsDispose = false; + if (state.index == index) { + handler = fetchHandler>( + disableTemporary: true, + editable: editable, + ); + } else if (state.toggleableHandlers.containsKey(index)) { + handler = state.toggleableHandlers[index]; + } + if (handler == null) { + List tools = const []; + final blocState = bloc.state; + if (blocState is DocumentLoaded) tools = blocState.info.tools; + final tool = tools.elementAtOrNull(index) ?? HandTool(); + handler = Handler.fromTool(tool); + needsDispose = true; + } + final result = callback(handler); + if (needsDispose) { + if (result is Future) { + result.then((value) => handler?.dispose(bloc)); + } else { + handler.dispose(bloc); + } + } + return result; + } + + Future changeTool( + EditorController controller, + DocumentBloc bloc, { + int? index, + BuildContext? context, + Handler? handler, + bool allowBake = true, + }) async { + talker.verbose('Changing tool to index: $index'); + await resetInput(bloc, controller.inputCubit); + final blocState = bloc.state; + if (blocState is! DocumentLoadSuccess) return null; + if (controller.saveCubit.state.embedding?.editable == false) { + return null; + } + final document = blocState.data; + final info = blocState.info; + index ??= state.index ?? 0; + if (handler == null && (index < 0 || index >= info.tools.length)) { + return null; + } + handler ??= Handler.fromTool(info.tools[index]); + var selectState = SelectState.normal; + if (context != null) { + selectState = await handler.onSelected(context); + } + if (selectState != SelectState.none) { + state.handler.dispose(bloc); + state.temporaryHandler?.dispose(bloc); + disposeTemporaryForegrounds(); + disposeForegrounds(); + final foregrounds = handler.createForegrounds( + controller, + document, + blocState.page, + info, + blocState.currentArea, + ); + if (handler.setupForegrounds) { + await Future.wait( + foregrounds.map( + (e) async => await e.setup( + controller.transformCubit, + document, + blocState.assetService, + blocState.page, + ), + ), + ); + } + if (selectState == SelectState.normal) { + controller.editorSessionCubit?.updateSelectedTool(handler.data, index); + setActiveTool( + index: index, + handler: handler, + cursor: handler.cursor ?? MouseCursor.defer, + foregrounds: foregrounds, + toolbar: await handler.getToolbar(bloc), + rendererStates: handler.rendererStates, + ); + controller.rendererCubit.setRendererStates( + rendererStates: handler.rendererStates, + temporaryRendererStates: const {}, + ); + if (allowBake) { + await controller.rendererCubit.bake(controller, blocState); + } + } else { + if (isHandlerEnabled(index)) { + disableHandler(bloc, index); + } else { + setToggleable( + handlers: {...state.toggleableHandlers, index: handler}, + foregrounds: {...state.toggleableForegrounds, index: foregrounds}, + ); + } + } + } + return handler; + } + + Future toggleHandler( + EditorController controller, + DocumentBloc bloc, + int index, + ) async { + if (state.toggleableHandlers.containsKey(index)) { + disableHandler(bloc, index); + } else { + await enableHandler(controller, bloc, index); + } + } + + Future enableHandler( + EditorController controller, + DocumentBloc bloc, + int index, + ) async { + final blocState = bloc.state; + if (blocState is! DocumentLoaded) return null; + if (index < 0 || index >= blocState.info.tools.length) { + return null; + } + final tool = blocState.info.tools[index]; + final handler = Handler.fromTool(tool); + final document = blocState.data; + final page = blocState.page; + final info = blocState.info; + final currentArea = blocState.currentArea; + final foregrounds = handler.createForegrounds( + controller, + document, + page, + info, + currentArea, + ); + if (handler.setupForegrounds) { + await Future.wait( + foregrounds.map( + (e) async => await e.setup( + controller.transformCubit, + document, + blocState.assetService, + page, + ), + ), + ); + } + setToggleable( + handlers: Map.from(state.toggleableHandlers)..[index] = handler, + foregrounds: Map.from(state.toggleableForegrounds)..[index] = foregrounds, + ); + return handler; + } + + bool disableHandler(DocumentBloc bloc, int index) { + final handler = state.toggleableHandlers[index]; + if (handler == null) { + return false; + } + handler.dispose(bloc); + final foregrounds = Map>.from( + state.toggleableForegrounds, + ); + final current = foregrounds.remove(index); + for (final r in current ?? []) { + r.dispose(); + } + setToggleable( + handlers: Map.from(state.toggleableHandlers)..remove(index), + foregrounds: foregrounds, + ); + return true; + } + + bool isHandlerEnabled(int index) => + state.toggleableHandlers.containsKey(index); + + void reset(EditorController controller, DocumentBloc bloc) { + for (final r in controller.rendererCubit.renderers) { + r.dispose(); + } + controller.rendererCubit.initializedElements.clear(); + state.handler.dispose(bloc); + state.temporaryHandler?.dispose(bloc); + for (var e in state.toggleableHandlers.values) { + e.dispose(bloc); + } + disposeAllForegrounds(); + resetRuntime(); + controller.rendererCubit.replace(const RendererRuntimeState()); + } + + Future changeTemporaryHandlerIndex( + BuildContext context, + EditorController controller, + int index, { + DocumentBloc? bloc, + TemporaryState temporaryState = TemporaryState.allowClick, + bool force = false, + }) async { + bloc ??= context.read(); + final blocState = bloc.state; + if (blocState is! DocumentLoadSuccess) return null; + if (index < 0 || index >= blocState.info.tools.length) { + return null; + } + final tool = blocState.info.tools[index]; + final temporaryHandler = state.temporaryHandler; + if (!force && index == state.temporaryIndex && temporaryHandler != null) { + return temporaryHandler; + } + return changeTemporaryHandler( + context, + controller, + tool, + bloc: bloc, + temporaryState: temporaryState, + index: index, + ); + } + + Future?> changeTemporaryHandler( + BuildContext context, + EditorController controller, + T tool, { + DocumentBloc? bloc, + int? index, + TemporaryState temporaryState = TemporaryState.allowClick, + }) async { + bloc ??= context.read(); + final handler = Handler.fromTool(tool); + final blocState = bloc.state; + if (blocState is! DocumentLoadSuccess) return null; + final document = blocState.data; + final page = blocState.page; + final currentArea = blocState.currentArea; + state.temporaryHandler?.dispose(bloc); + final selectState = await handler.onSelected(context); + + if (selectState == SelectState.normal) { + disposeTemporaryForegrounds(); + final temporaryForegrounds = handler.createForegrounds( + controller, + document, + page, + blocState.info, + currentArea, + ); + if (handler.setupForegrounds) { + await Future.wait( + temporaryForegrounds.map( + (e) async => await e.setup( + controller.transformCubit, + document, + blocState.assetService, + page, + ), + ), + ); + } + setTemporaryTool( + handler: handler, + index: index, + foregrounds: temporaryForegrounds, + toolbar: await handler.getToolbar(bloc), + cursor: handler.cursor, + rendererStates: handler.rendererStates, + temporaryState: temporaryState, + ); + controller.rendererCubit.setRendererStates( + temporaryRendererStates: handler.rendererStates, + ); + await controller.rendererCubit.bake(controller, blocState); + } else if (selectState == SelectState.toggle && index != null) { + await toggleHandler(controller, bloc, index); + } + return handler; + } + + void resetReleaseHandler(DocumentBloc bloc, [RendererCubit? rendererCubit]) { + if (state.temporaryState == TemporaryState.removeAfterRelease) { + resetTemporaryHandler(bloc, true, rendererCubit); + } + } + + void resetDownHandler(DocumentBloc bloc, [RendererCubit? rendererCubit]) { + resetTemporaryHandler(bloc, false, rendererCubit); + } + + void resetTemporaryHandler( + DocumentBloc bloc, [ + bool force = false, + RendererCubit? rendererCubit, + ]) { + if (state.temporaryHandler == null) { + return; + } + if (!force && state.temporaryState != TemporaryState.removeAfterClick) { + if (state.temporaryState == TemporaryState.allowClick) { + setTemporaryState(TemporaryState.removeAfterClick); + } + return; + } + state.temporaryHandler?.dispose(bloc); + disposeTemporaryForegrounds(); + setTemporaryTool( + handler: null, + index: null, + foregrounds: null, + toolbar: null, + cursor: null, + rendererStates: null, + ); + rendererCubit?.setRendererStates(temporaryRendererStates: const {}); + } + + Future refresh( + EditorController controller, + DocumentLoaded blocState, { + bool allowBake = true, + }) async { + talker.verbose('Refreshing tools'); + final document = blocState.data; + final page = blocState.page; + final info = blocState.info; + final assetService = blocState.assetService; + final currentArea = blocState.currentArea; + const mapEq = MapEquality(); + if (!controller.isClosed) { + disposeAllForegrounds(); + final temporaryForegrounds = state.temporaryHandler?.createForegrounds( + controller, + document, + page, + info, + currentArea, + ); + if (temporaryForegrounds != null && + state.temporaryHandler?.setupForegrounds == true) { + await Future.wait( + temporaryForegrounds.map( + (e) async => await e.setup( + controller.transformCubit, + document, + assetService, + page, + ), + ), + ); + } + final foregrounds = state.handler.createForegrounds( + controller, + document, + page, + info, + currentArea, + ); + if (state.handler.setupForegrounds) { + await Future.wait( + foregrounds.map( + (e) async => await e.setup( + controller.transformCubit, + document, + assetService, + page, + ), + ), + ); + } + final toggleableForegrounds = >{}; + for (final entry in state.toggleableHandlers.entries) { + final handler = entry.value; + final index = entry.key; + final foregrounds = handler.createForegrounds( + controller, + document, + page, + info, + currentArea, + ); + if (handler.setupForegrounds) { + await Future.wait( + foregrounds.map( + (e) async => await e.setup( + controller.transformCubit, + document, + assetService, + page, + ), + ), + ); + } + toggleableForegrounds[index] = foregrounds; + } + final rendererStates = state.handler.rendererStates; + final temporaryRendererStates = state.temporaryHandler?.rendererStates; + final statesChanged = !mapEq.equals( + controller.rendererCubit.state.rendererStates, + rendererStates, + ); + final temporaryStatesChanged = !mapEq.equals( + controller.rendererCubit.state.temporaryRendererStates, + temporaryRendererStates, + ); + final shouldBake = statesChanged || temporaryStatesChanged; + setForegrounds( + temporaryForegrounds: temporaryForegrounds, + toggleableForegrounds: toggleableForegrounds, + foregrounds: foregrounds, + cursor: state.handler.cursor ?? MouseCursor.defer, + temporaryCursor: state.temporaryHandler?.cursor, + ); + controller.rendererCubit.setRendererStates( + rendererStates: statesChanged + ? rendererStates + : controller.rendererCubit.state.rendererStates, + temporaryRendererStates: temporaryStatesChanged + ? temporaryRendererStates + : controller.rendererCubit.state.temporaryRendererStates, + ); + if (allowBake) { + if (shouldBake) { + return controller.rendererCubit.bake( + controller, + blocState, + reset: true, + ); + } else if (!controller.rendererCubit.state.cameraViewport.baked) { + return controller.rendererCubit.delayedBake(controller, blocState); + } + } + } + } + + Future refreshToolbar(DocumentBloc bloc) async { + final toolbar = await state.handler.getToolbar(bloc); + final temporaryToolbar = await state.temporaryHandler?.getToolbar(bloc); + setToolbar(toolbar: toolbar, temporaryToolbar: temporaryToolbar); + } + + Future refreshForegrounds( + EditorController controller, + DocumentLoaded blocState, + ) => foregroundRefreshRunner.schedule( + () => _refreshForegrounds(controller, blocState), + ); + + Future _refreshForegrounds( + EditorController controller, + DocumentLoaded blocState, + ) async { + if (controller.isClosed) return; + final document = blocState.data; + final page = blocState.page; + final info = blocState.info; + final assetService = blocState.assetService; + final currentArea = blocState.currentArea; + + disposeForegrounds(); + disposeTemporaryForegrounds(); + + final temporaryForegrounds = state.temporaryHandler?.createForegrounds( + controller, + document, + page, + info, + currentArea, + ); + if (temporaryForegrounds != null && + temporaryForegrounds.isNotEmpty && + state.temporaryHandler?.setupForegrounds == true) { + await Future.wait( + temporaryForegrounds.map( + (e) async => await e.setup( + controller.transformCubit, + document, + assetService, + page, + ), + ), + ); + } + + final foregrounds = state.handler.createForegrounds( + controller, + document, + page, + info, + currentArea, + ); + if (foregrounds.isNotEmpty && state.handler.setupForegrounds) { + await Future.wait( + foregrounds.map( + (e) async => await e.setup( + controller.transformCubit, + document, + assetService, + page, + ), + ), + ); + } + + const mapEq = MapEquality(); + final rendererStates = state.handler.rendererStates; + final temporaryRendererStates = state.temporaryHandler?.rendererStates; + final statesChanged = !mapEq.equals( + controller.rendererCubit.state.rendererStates, + rendererStates, + ); + final temporaryStatesChanged = !mapEq.equals( + controller.rendererCubit.state.temporaryRendererStates, + temporaryRendererStates, + ); + + setForegrounds( + foregrounds: foregrounds, + temporaryForegrounds: temporaryForegrounds, + cursor: state.handler.cursor ?? MouseCursor.defer, + temporaryCursor: state.temporaryHandler?.cursor, + ); + controller.rendererCubit.setRendererStates( + rendererStates: statesChanged + ? rendererStates + : controller.rendererCubit.state.rendererStates, + temporaryRendererStates: temporaryStatesChanged + ? temporaryRendererStates + : controller.rendererCubit.state.temporaryRendererStates, + ); + + if (statesChanged || temporaryStatesChanged) { + await controller.rendererCubit.bake(controller, blocState, reset: true); + } + } + + void updateIndex(EditorController controller, DocumentBloc bloc) { + final docState = bloc.state; + if (docState is! DocumentLoadSuccess) return; + final info = docState.info; + final index = info.tools.indexOf(state.handler.data); + if (index < 0) { + changeTool(controller, bloc, index: state.index ?? 0); + } + if (index == state.index) { + return; + } + setIndex(index); + final selection = state.selection; + if (selection?.selected.contains(state.handler.data) ?? false) { + resetSelection(); + } + } + + Future disposeRuntime(dynamic bloc) async { + state.handler.dispose(bloc); + state.temporaryHandler?.dispose(bloc); + for (final handler in state.toggleableHandlers.values) { + handler.dispose(bloc); + } + for (final renderer in state.getAllForegrounds()) { + renderer.dispose(); + } + foregroundRefreshRunner.cancel(); + await foregroundRefreshRunner.disposeAndWait(); + _networkingDebounceTimer?.cancel(); + _networkingDebounceTimer = null; + _controller = null; + } + + @override + Future close() { + _networkingDebounceTimer?.cancel(); + _networkingDebounceTimer = null; + _controller = null; + return super.close(); + } +} diff --git a/app/lib/cubits/editor_view.dart b/app/lib/cubits/editor_view.dart new file mode 100644 index 000000000000..a8611108a838 --- /dev/null +++ b/app/lib/cubits/editor_view.dart @@ -0,0 +1,82 @@ +part of 'editor_runtime.dart'; + +@freezed +sealed class EditorViewState with _$EditorViewState { + const factory EditorViewState({ + @Default(UtilitiesState()) UtilitiesState utilities, + @Default(ViewOption()) ViewOption viewOption, + @Default(true) bool areaNavigatorCreate, + @Default(true) bool areaNavigatorExact, + @Default(false) bool areaNavigatorAsk, + @Default(false) bool navigatorEnabled, + @Default(NavigatorPage.waypoints) NavigatorPage navigatorPage, + @Default('') String userName, + }) = _EditorViewState; +} + +class EditorViewCubit extends Cubit { + EditorViewCubit({this.editorSessionCubit, EditorViewState? initial}) + : super(initial ?? const EditorViewState()); + + final EditorSessionCubit? editorSessionCubit; + ToolCubit? _toolCubit; + + void bindToolCubit(ToolCubit toolCubit) { + _toolCubit = toolCubit; + } + + @override + void onChange(Change change) { + super.onChange(change); + if (change.nextState.userName != change.currentState.userName) { + _toolCubit?.scheduleNetworkingState(); + } + } + + void replace(EditorViewState state) => emit(state); + + void updateUtilities({UtilitiesState? utilities, ViewOption? view}) { + emit( + state.copyWith( + utilities: utilities ?? state.utilities, + viewOption: view ?? state.viewOption, + ), + ); + if (utilities != null) { + editorSessionCubit?.updateUtilities(utilities); + } + } + + void setAreaNavigator({bool? create, bool? exact, bool? ask}) { + emit( + state.copyWith( + areaNavigatorCreate: create ?? state.areaNavigatorCreate, + areaNavigatorExact: exact ?? state.areaNavigatorExact, + areaNavigatorAsk: ask ?? state.areaNavigatorAsk, + ), + ); + editorSessionCubit?.updateAreaNavigator( + create: create, + exact: exact, + ask: ask, + ); + } + + void setNavigator({bool? enabled, NavigatorPage? page}) { + emit( + state.copyWith( + navigatorEnabled: enabled ?? state.navigatorEnabled, + navigatorPage: page ?? state.navigatorPage, + ), + ); + editorSessionCubit?.updateNavigator(enabled: enabled, page: page); + } + + void setUserName(String name) => emit(state.copyWith(userName: name)); + + @override + Future close() { + _toolCubit = null; + return super.close(); + } +} diff --git a/app/lib/cubits/transform.dart b/app/lib/cubits/transform.dart index 4d13765394df..ee28c40e3608 100644 --- a/app/lib/cubits/transform.dart +++ b/app/lib/cubits/transform.dart @@ -227,37 +227,30 @@ class TransformCubit extends Cubit { return Rect.fromLTRB(minX, minY, maxX, maxY); } - bool _isNavigationRailVisible( - SettingsCubit settingsCubit, - RendererCubit rendererCubit, - EditorInputCubit inputCubit, - ) { - final settings = settingsCubit.state; - final viewport = rendererCubit.state.cameraViewport; + bool _isNavigationRailVisible(EditorRuntimeContext runtime) { + final settings = runtime.settingsCubit.state; + final viewport = runtime.rendererCubit.state.cameraViewport; return settings.navigationRail && settings.navigatorPosition == NavigatorPosition.left && - inputCubit.state.hideUi == HideState.visible && + runtime.inputCubit.state.hideUi == HideState.visible && (viewport.width ?? 0) >= LeapBreakpoints.expanded && (viewport.height ?? 0) >= 400; } Rect? calculateViewportBounds({ - required SettingsCubit settingsCubit, - required RendererCubit rendererCubit, - required EditorInputCubit inputCubit, + required EditorRuntimeContext runtime, Area? currentArea, CameraTransform? customTransform, }) { - final settings = settingsCubit.state; + final settings = runtime.settingsCubit.state; var multiplier = settings.limitViewportMultiplier; final positive = settings.limitViewportPositive; if (multiplier == null && !positive && currentArea == null) return null; - final viewport = rendererCubit.state.cameraViewport; + final viewport = runtime.rendererCubit.state.cameraViewport; final transform = customTransform ?? state; - final navigationRailOffset = - _isNavigationRailVisible(settingsCubit, rendererCubit, inputCubit) + final navigationRailOffset = _isNavigationRailVisible(runtime) ? kNavigationRailWidth / transform.size : 0.0; final size = @@ -267,7 +260,7 @@ class TransformCubit extends Cubit { ) / settings.renderResolution.multiplier; - final contentRect = getContentRect(rendererCubit, currentArea); + final contentRect = getContentRect(runtime.rendererCubit, currentArea); double minX = double.negativeInfinity; double minY = double.negativeInfinity; @@ -308,14 +301,10 @@ class TransformCubit extends Cubit { CameraTransform _clampTransform({ required CameraTransform transform, - required SettingsCubit settingsCubit, - required RendererCubit rendererCubit, - required EditorInputCubit inputCubit, + required EditorRuntimeContext runtime, }) { final bounds = calculateViewportBounds( - settingsCubit: settingsCubit, - rendererCubit: rendererCubit, - inputCubit: inputCubit, + runtime: runtime, customTransform: transform, ); if (bounds == null) return transform; @@ -362,14 +351,10 @@ class TransformCubit extends Cubit { required Area area, required int dx, required int dy, - required SettingsCubit settingsCubit, - required RendererCubit rendererCubit, - required EditorInputCubit inputCubit, + required EditorRuntimeContext runtime, }) { final newBounds = calculateViewportBounds( - settingsCubit: settingsCubit, - rendererCubit: rendererCubit, - inputCubit: inputCubit, + runtime: runtime, currentArea: area, ); if (newBounds == null) return; @@ -396,10 +381,7 @@ class TransformCubit extends Cubit { Future navigateToRelativeArea({ required DocumentBloc bloc, - required SettingsCubit settingsCubit, - required RendererCubit rendererCubit, - required EditorInputCubit inputCubit, - required EditorViewCubit viewCubit, + required EditorRuntimeContext runtime, required int dx, required int dy, Future Function()? createAreaName, @@ -412,25 +394,21 @@ class TransformCubit extends Cubit { var area = getRelativeArea( docState: docState, - viewCubit: viewCubit, + viewCubit: runtime.viewCubit, currentArea: current, dx: dx, dy: dy, ); if (area != null) { bloc.add(CurrentAreaChanged(area.name)); - teleportToAreaEdge( - area: area, - dx: dx, - dy: dy, - settingsCubit: settingsCubit, - rendererCubit: rendererCubit, - inputCubit: inputCubit, - ); + teleportToAreaEdge(area: area, dx: dx, dy: dy, runtime: runtime); return; } - if (!viewCubit.state.areaNavigatorCreate || createAreaName == null) return; + if (!runtime.viewCubit.state.areaNavigatorCreate || + createAreaName == null) { + return; + } final name = await createAreaName(); if (name == null) return; @@ -447,35 +425,23 @@ class TransformCubit extends Cubit { ); bloc.add(AreasCreated([AreaPreset(area: newArea)])); bloc.add(CurrentAreaChanged(name)); - teleportToAreaEdge( - area: newArea, - dx: dx, - dy: dy, - settingsCubit: settingsCubit, - rendererCubit: rendererCubit, - inputCubit: inputCubit, - ); + teleportToAreaEdge(area: newArea, dx: dx, dy: dy, runtime: runtime); } void moveConstrained( Offset delta, { - required SettingsCubit settingsCubit, - required RendererCubit rendererCubit, - required EditorInputCubit inputCubit, - required EditorViewCubit viewCubit, + required EditorRuntimeContext runtime, DocumentBloc? bloc, bool force = false, Area? currentArea, }) { - final utilitiesState = viewCubit.state.utilities; + final utilitiesState = runtime.viewCubit.state.utilities; if (!force) { if (utilitiesState.lockHorizontal) delta = Offset(0, delta.dy); if (utilitiesState.lockVertical) delta = Offset(delta.dx, 0); final bounds = calculateViewportBounds( - settingsCubit: settingsCubit, - rendererCubit: rendererCubit, - inputCubit: inputCubit, + runtime: runtime, currentArea: currentArea, ); if (bounds != null) { @@ -505,24 +471,17 @@ class TransformCubit extends Cubit { } if ((dx != 0 || dy != 0) && - settingsCubit.state.hasFlag('edgePanAreaSwitching')) { + runtime.settingsCubit.state.hasFlag('edgePanAreaSwitching')) { final area = getRelativeArea( docState: docState, - viewCubit: viewCubit, + viewCubit: runtime.viewCubit, currentArea: currentArea, dx: dx, dy: dy, ); if (area != null) { bloc?.add(CurrentAreaChanged(area.name)); - teleportToAreaEdge( - area: area, - dx: dx, - dy: dy, - settingsCubit: settingsCubit, - rendererCubit: rendererCubit, - inputCubit: inputCubit, - ); + teleportToAreaEdge(area: area, dx: dx, dy: dy, runtime: runtime); return; } } @@ -540,14 +499,11 @@ class TransformCubit extends Cubit { void zoomConstrained( double delta, { - required SettingsCubit settingsCubit, - required RendererCubit rendererCubit, - required EditorInputCubit inputCubit, - required EditorViewCubit viewCubit, + required EditorRuntimeContext runtime, Offset cursor = Offset.zero, bool force = false, }) { - final utilitiesState = viewCubit.state.utilities; + final utilitiesState = runtime.viewCubit.state.utilities; if (utilitiesState.lockZoom && !force) { delta = 1; } @@ -559,25 +515,17 @@ class TransformCubit extends Cubit { return; } final transform = state.withSize(state.size * delta, cursor); - final clamped = _clampTransform( - transform: transform, - settingsCubit: settingsCubit, - rendererCubit: rendererCubit, - inputCubit: inputCubit, - ); + final clamped = _clampTransform(transform: transform, runtime: runtime); teleport(clamped.position, clamped.size); } void sizeConstrained( double size, { - required SettingsCubit settingsCubit, - required RendererCubit rendererCubit, - required EditorInputCubit inputCubit, - required EditorViewCubit viewCubit, + required EditorRuntimeContext runtime, Offset cursor = Offset.zero, bool force = false, }) { - final utilitiesState = viewCubit.state.utilities; + final utilitiesState = runtime.viewCubit.state.utilities; if (utilitiesState.lockZoom && !force) return; if (force) { this.size(size, cursor); @@ -585,9 +533,7 @@ class TransformCubit extends Cubit { } final transform = _clampTransform( transform: state.withSize(size, cursor), - settingsCubit: settingsCubit, - rendererCubit: rendererCubit, - inputCubit: inputCubit, + runtime: runtime, ); teleport(transform.position, transform.size); } @@ -595,16 +541,13 @@ class TransformCubit extends Cubit { void slideConstrained( Offset positionVelocity, double sizeVelocity, { - required SettingsCubit settingsCubit, - required RendererCubit rendererCubit, - required EditorInputCubit inputCubit, - required EditorViewCubit viewCubit, + required EditorRuntimeContext runtime, bool force = false, Area? currentArea, }) { - final settings = settingsCubit.state; + final settings = runtime.settingsCubit.state; if (!settings.hasFlag('smoothNavigation')) return; - final utilitiesState = viewCubit.state.utilities; + final utilitiesState = runtime.viewCubit.state.utilities; Rect? bounds; var outOfBounds = false; if (!force) { @@ -617,9 +560,7 @@ class TransformCubit extends Cubit { if (utilitiesState.lockZoom) sizeVelocity = 0; bounds = calculateViewportBounds( - settingsCubit: settingsCubit, - rendererCubit: rendererCubit, - inputCubit: inputCubit, + runtime: runtime, currentArea: currentArea, ); if (bounds != null) { diff --git a/app/lib/cubits/transform.freezed.dart b/app/lib/cubits/transform.freezed.dart index b25786ee3622..d8f0184bd093 100644 --- a/app/lib/cubits/transform.freezed.dart +++ b/app/lib/cubits/transform.freezed.dart @@ -12,7 +12,7 @@ part of 'transform.dart'; // dart format off T _$identity(T value) => value; /// @nodoc -mixin _$FrictionState { +mixin _$FrictionState implements DiagnosticableTreeMixin { Offset get beginOffset; double get beginSize; DateTime get lastUpdate; double get duration; /// Create a copy of FrictionState @@ -22,6 +22,12 @@ mixin _$FrictionState { $FrictionStateCopyWith get copyWith => _$FrictionStateCopyWithImpl(this as FrictionState, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'FrictionState')) + ..add(DiagnosticsProperty('beginOffset', beginOffset))..add(DiagnosticsProperty('beginSize', beginSize))..add(DiagnosticsProperty('lastUpdate', lastUpdate))..add(DiagnosticsProperty('duration', duration)); +} @override bool operator ==(Object other) { @@ -33,7 +39,7 @@ bool operator ==(Object other) { int get hashCode => Object.hash(runtimeType,beginOffset,beginSize,lastUpdate,duration); @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'FrictionState(beginOffset: $beginOffset, beginSize: $beginSize, lastUpdate: $lastUpdate, duration: $duration)'; } @@ -79,7 +85,7 @@ as double, /// @nodoc -class _FrictionState implements FrictionState { +class _FrictionState with DiagnosticableTreeMixin implements FrictionState { const _FrictionState(this.beginOffset, this.beginSize, this.lastUpdate, this.duration); @@ -95,6 +101,12 @@ class _FrictionState implements FrictionState { _$FrictionStateCopyWith<_FrictionState> get copyWith => __$FrictionStateCopyWithImpl<_FrictionState>(this, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'FrictionState')) + ..add(DiagnosticsProperty('beginOffset', beginOffset))..add(DiagnosticsProperty('beginSize', beginSize))..add(DiagnosticsProperty('lastUpdate', lastUpdate))..add(DiagnosticsProperty('duration', duration)); +} @override bool operator ==(Object other) { @@ -106,7 +118,7 @@ bool operator ==(Object other) { int get hashCode => Object.hash(runtimeType,beginOffset,beginSize,lastUpdate,duration); @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'FrictionState(beginOffset: $beginOffset, beginSize: $beginSize, lastUpdate: $lastUpdate, duration: $duration)'; } @@ -149,7 +161,7 @@ as double, } /// @nodoc -mixin _$CameraTransform { +mixin _$CameraTransform implements DiagnosticableTreeMixin { double get pixelRatio; Offset get position; double get size; FrictionState? get friction; /// Create a copy of CameraTransform @@ -159,6 +171,12 @@ mixin _$CameraTransform { $CameraTransformCopyWith get copyWith => _$CameraTransformCopyWithImpl(this as CameraTransform, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'CameraTransform')) + ..add(DiagnosticsProperty('pixelRatio', pixelRatio))..add(DiagnosticsProperty('position', position))..add(DiagnosticsProperty('size', size))..add(DiagnosticsProperty('friction', friction)); +} @override bool operator ==(Object other) { @@ -170,7 +188,7 @@ bool operator ==(Object other) { int get hashCode => Object.hash(runtimeType,pixelRatio,position,size,friction); @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'CameraTransform(pixelRatio: $pixelRatio, position: $position, size: $size, friction: $friction)'; } @@ -228,7 +246,7 @@ $FrictionStateCopyWith<$Res>? get friction { /// @nodoc -class _CameraTransform extends CameraTransform { +class _CameraTransform extends CameraTransform with DiagnosticableTreeMixin { const _CameraTransform([this.pixelRatio = 1, this.position = Offset.zero, this.size = 1, this.friction]): super._(); @@ -244,6 +262,12 @@ class _CameraTransform extends CameraTransform { _$CameraTransformCopyWith<_CameraTransform> get copyWith => __$CameraTransformCopyWithImpl<_CameraTransform>(this, _$identity); +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'CameraTransform')) + ..add(DiagnosticsProperty('pixelRatio', pixelRatio))..add(DiagnosticsProperty('position', position))..add(DiagnosticsProperty('size', size))..add(DiagnosticsProperty('friction', friction)); +} @override bool operator ==(Object other) { @@ -255,7 +279,7 @@ bool operator ==(Object other) { int get hashCode => Object.hash(runtimeType,pixelRatio,position,size,friction); @override -String toString() { +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { return 'CameraTransform(pixelRatio: $pixelRatio, position: $position, size: $size, friction: $friction)'; } diff --git a/app/lib/helpers/point.dart b/app/lib/helpers/point.dart index 7c6ee4378315..923dfc6b3eaf 100644 --- a/app/lib/helpers/point.dart +++ b/app/lib/helpers/point.dart @@ -1,43 +1,43 @@ -import 'dart:math'; -import 'dart:ui'; - -import 'package:butterfly/cubits/transform.dart'; -import 'package:butterfly_api/butterfly_api.dart'; -import 'package:material_leap/material_leap.dart'; -import 'package:perfect_freehand/perfect_freehand.dart' as freehand; -import 'package:vector_math/vector_math.dart' show Vector2; - -extension PointHelper on Point { - Offset toOffset() => Offset(x, y); -} - -extension OffsetHelper on Offset { - String roundedX() => dx.toStringAsFixed(kRoundPrecision); - String roundedY() => dy.toStringAsFixed(kRoundPrecision); - String roundedBetweenX(Offset p) => - ((dx + p.dx) / 2).toStringAsFixed(kRoundPrecision); - String roundedBetweenY(Offset p) => - ((dy + p.dy) / 2).toStringAsFixed(kRoundPrecision); -} - -extension PathPointHelper on PathPoint { - static PathPoint fromVector(Vector2 vector, [double pressure = 1]) => - PathPoint(vector.x, vector.y, pressure); - - Vector2 toVector() => Vector2(x, y); - - freehand.PointVector toFreehandPoint([double thinning = 1]) => - freehand.PointVector(x, y, pressure * thinning); - - Offset toOffset() => Offset(x, y); - - PathPoint scale(double zoom, Offset center) => PathPoint.fromPoint( - toOffset().scaleFromCenter(zoom, center).toPoint(), - pressure, - ); - - PathPoint rotate(Offset center, double angle) { - final rotated = toOffset().rotate(center, angle); - return PathPoint(rotated.dx, rotated.dy, pressure); - } -} +import 'dart:math'; +import 'dart:ui'; + +import 'package:butterfly/cubits/transform.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:material_leap/material_leap.dart'; +import 'package:perfect_freehand/perfect_freehand.dart' as freehand; +import 'package:vector_math/vector_math.dart' show Vector2; + +extension PointHelper on Point { + Offset toOffset() => Offset(x, y); +} + +extension OffsetHelper on Offset { + String roundedX() => dx.toStringAsFixed(kRoundPrecision); + String roundedY() => dy.toStringAsFixed(kRoundPrecision); + String roundedBetweenX(Offset p) => + ((dx + p.dx) / 2).toStringAsFixed(kRoundPrecision); + String roundedBetweenY(Offset p) => + ((dy + p.dy) / 2).toStringAsFixed(kRoundPrecision); +} + +extension PathPointHelper on PathPoint { + static PathPoint fromVector(Vector2 vector, [double pressure = 1]) => + PathPoint(vector.x, vector.y, pressure); + + Vector2 toVector() => Vector2(x, y); + + freehand.PointVector toFreehandPoint([double thinning = 1]) => + freehand.PointVector(x, y, pressure * thinning); + + Offset toOffset() => Offset(x, y); + + PathPoint scale(double zoom, Offset center) => PathPoint.fromPoint( + toOffset().scaleFromCenter(zoom, center).toPoint(), + pressure, + ); + + PathPoint rotate(Offset center, double angle) { + final rotated = toOffset().rotate(center, angle); + return PathPoint(rotated.dx, rotated.dy, pressure); + } +} diff --git a/app/lib/selections/document.dart b/app/lib/selections/document.dart index 8bdc30a7f6fd..a30b44ccb852 100644 --- a/app/lib/selections/document.dart +++ b/app/lib/selections/document.dart @@ -418,10 +418,7 @@ class _UtilitiesViewState extends State<_UtilitiesView> editorController.transformCubit.sizeConstrained( value / 100, cursor: Offset(size.width / 2, size.height / 2), - settingsCubit: editorController.settingsCubit, - rendererCubit: editorController.rendererCubit, - inputCubit: editorController.inputCubit, - viewCubit: editorController.viewCubit, + runtime: editorController, ); context.read().bake(); }, diff --git a/app/lib/views/navigator/areas.dart b/app/lib/views/navigator/areas.dart index 6c3e87f1b8f1..c4267c956573 100644 --- a/app/lib/views/navigator/areas.dart +++ b/app/lib/views/navigator/areas.dart @@ -294,10 +294,7 @@ class _AreasViewState extends State { Future navigateToRelativeArea(int dx, int dy) async { await editorController.transformCubit.navigateToRelativeArea( bloc: context.read(), - settingsCubit: editorController.settingsCubit, - rendererCubit: editorController.rendererCubit, - inputCubit: editorController.inputCubit, - viewCubit: editorController.viewCubit, + runtime: editorController, dx: dx, dy: dy, createAreaName: () => createAreaName( diff --git a/app/lib/views/view.dart b/app/lib/views/view.dart index e0fef515b352..2e8f1004a407 100644 --- a/app/lib/views/view.dart +++ b/app/lib/views/view.dart @@ -343,10 +343,7 @@ class _MainViewViewportState extends State final transform = context.read().state; cubit.transformCubit.moveConstrained( -event.delta / transform.size, - settingsCubit: cubit.settingsCubit, - rendererCubit: cubit.rendererCubit, - inputCubit: cubit.inputCubit, - viewCubit: cubit.viewCubit, + runtime: cubit, bloc: context.read(), currentArea: state.currentArea, ); @@ -663,10 +660,7 @@ class _MainViewViewportState extends State -details.focalPointDelta / sensitivity / cubit.transformCubit.state.size, - settingsCubit: cubit.settingsCubit, - rendererCubit: cubit.rendererCubit, - inputCubit: cubit.inputCubit, - viewCubit: cubit.viewCubit, + runtime: cubit, bloc: bloc, currentArea: state.currentArea, ); @@ -674,10 +668,7 @@ class _MainViewViewportState extends State cubit.transformCubit.zoomConstrained( current / sensitivity + 1, cursor: point, - settingsCubit: cubit.settingsCubit, - rendererCubit: cubit.rendererCubit, - inputCubit: cubit.inputCubit, - viewCubit: cubit.viewCubit, + runtime: cubit, ); } size = details.scale; @@ -716,10 +707,7 @@ class _MainViewViewportState extends State sensitivity / cubit.transformCubit.state.size, details.scaleVelocity, - settingsCubit: cubit.settingsCubit, - rendererCubit: cubit.rendererCubit, - inputCubit: cubit.inputCubit, - viewCubit: cubit.viewCubit, + runtime: cubit, currentArea: state.currentArea, ); if (!settings.hasFlag( @@ -842,13 +830,7 @@ class _MainViewViewportState extends State scale, cursor: pointerSignal .localPosition, - settingsCubit: - cubit.settingsCubit, - rendererCubit: - cubit.rendererCubit, - inputCubit: - cubit.inputCubit, - viewCubit: cubit.viewCubit, + runtime: cubit, ); } else { cubit.transformCubit @@ -859,13 +841,7 @@ class _MainViewViewportState extends State ? Offset(dy, dx) : Offset(dx, dy)) / transform.size, - settingsCubit: - cubit.settingsCubit, - rendererCubit: - cubit.rendererCubit, - inputCubit: - cubit.inputCubit, - viewCubit: cubit.viewCubit, + runtime: cubit, bloc: bloc, currentArea: state.currentArea, @@ -875,13 +851,7 @@ class _MainViewViewportState extends State scale, cursor: pointerSignal .localPosition, - settingsCubit: - cubit.settingsCubit, - rendererCubit: - cubit.rendererCubit, - inputCubit: - cubit.inputCubit, - viewCubit: cubit.viewCubit, + runtime: cubit, ); } if (!settings.hasFlag( diff --git a/app/lib/views/zoom.dart b/app/lib/views/zoom.dart index 7e4a98bad116..ffbf109eb720 100644 --- a/app/lib/views/zoom.dart +++ b/app/lib/views/zoom.dart @@ -78,10 +78,7 @@ class _ZoomViewState extends State with TickerProviderStateMixin { value, cursor: center, force: true, - settingsCubit: editorController.settingsCubit, - rendererCubit: editorController.rendererCubit, - inputCubit: editorController.inputCubit, - viewCubit: editorController.viewCubit, + runtime: editorController, ); if (bake) { editorController.rendererCubit.bake(editorController, documentState); From de4d6c96500ce96bc924625a60f665c20cf918fa Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 30 Jun 2026 19:45:02 +0200 Subject: [PATCH 036/117] Remove leftover viewoption, make persitent state a repository and use more blocselector --- api/lib/butterfly_models.dart | 2 - api/lib/src/models/info.dart | 2 - api/lib/src/models/info.freezed.dart | 57 +- api/lib/src/models/info.g.dart | 4 - api/lib/src/models/utilities.dart | 24 - api/lib/src/models/utilities.freezed.dart | 81 -- api/lib/src/models/utilities.g.dart | 24 - api/lib/src/models/view.dart | 15 - api/lib/src/models/view.freezed.dart | 46 -- api/lib/src/models/view.g.dart | 12 - api/lib/src/protocol/event.dart | 6 - api/lib/src/protocol/event.freezed.dart | 172 ----- api/lib/src/protocol/event.g.dart | 16 - app/lib/bloc/document_state.dart | 19 +- app/lib/cubits/editor_controller.dart | 13 +- app/lib/cubits/editor_runtime.dart | 1 + app/lib/cubits/editor_runtime.freezed.dart | 75 +- app/lib/cubits/editor_session.dart | 103 +-- app/lib/cubits/editor_view.dart | 16 +- app/lib/cubits/settings.dart | 12 - app/lib/cubits/settings.freezed.dart | 57 +- app/lib/cubits/settings.g.dart | 6 - app/lib/cubits/transform.dart | 22 +- app/lib/handlers/eraser.dart | 6 +- app/lib/handlers/label.dart | 6 +- app/lib/handlers/polygon.dart | 6 +- app/lib/handlers/select.dart | 22 +- app/lib/models/persisted_document_state.dart | 104 ++- .../persisted_document_state.freezed.dart | 716 ++++++++++++++++-- .../models/persisted_document_state.g.dart | 141 +++- app/lib/repositories/document_state.dart | 48 ++ app/lib/selections/document.dart | 21 +- app/lib/selections/selection.dart | 1 + app/lib/views/app_bar.dart | 51 +- app/lib/views/edit.dart | 40 +- app/lib/views/main.dart | 77 +- app/lib/views/pen_only_toggle.dart | 37 +- app/lib/views/zoom.dart | 224 +++--- app/test/cubits/editor_session_test.dart | 55 +- 39 files changed, 1313 insertions(+), 1027 deletions(-) delete mode 100644 api/lib/src/models/utilities.dart delete mode 100644 api/lib/src/models/utilities.freezed.dart delete mode 100644 api/lib/src/models/utilities.g.dart delete mode 100644 api/lib/src/models/view.dart delete mode 100644 api/lib/src/models/view.freezed.dart delete mode 100644 api/lib/src/models/view.g.dart create mode 100644 app/lib/repositories/document_state.dart diff --git a/api/lib/butterfly_models.dart b/api/lib/butterfly_models.dart index 54a8fa2e39e6..5ee90eb314a8 100644 --- a/api/lib/butterfly_models.dart +++ b/api/lib/butterfly_models.dart @@ -17,8 +17,6 @@ export 'src/models/pack.dart'; export 'src/models/page.dart'; export 'src/models/texture.dart'; export 'src/models/tool.dart'; -export 'src/models/utilities.dart'; export 'src/models/point.dart'; export 'src/models/property.dart'; -export 'src/models/view.dart'; export 'src/models/waypoint.dart'; diff --git a/api/lib/src/models/info.dart b/api/lib/src/models/info.dart index 32559167d403..4a2ced297c4b 100644 --- a/api/lib/src/models/info.dart +++ b/api/lib/src/models/info.dart @@ -3,7 +3,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'export.dart'; import 'tool.dart'; -import 'view.dart'; part 'info.freezed.dart'; part 'info.g.dart'; @@ -13,7 +12,6 @@ sealed class DocumentInfo with _$DocumentInfo { const factory DocumentInfo({ @Default([]) List tools, @Default([]) List exportPresets, - @Default(ViewOption()) ViewOption view, @Default({}) Map extra, }) = _DocumentInfo; diff --git a/api/lib/src/models/info.freezed.dart b/api/lib/src/models/info.freezed.dart index ad75bca53bf4..c45be6b01565 100644 --- a/api/lib/src/models/info.freezed.dart +++ b/api/lib/src/models/info.freezed.dart @@ -15,7 +15,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$DocumentInfo { - List get tools; List get exportPresets; ViewOption get view; Map get extra; + List get tools; List get exportPresets; Map get extra; /// Create a copy of DocumentInfo /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -28,16 +28,16 @@ $DocumentInfoCopyWith get copyWith => _$DocumentInfoCopyWithImpl Object.hash(runtimeType,const DeepCollectionEquality().hash(tools),const DeepCollectionEquality().hash(exportPresets),view,const DeepCollectionEquality().hash(extra)); +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(tools),const DeepCollectionEquality().hash(exportPresets),const DeepCollectionEquality().hash(extra)); @override String toString() { - return 'DocumentInfo(tools: $tools, exportPresets: $exportPresets, view: $view, extra: $extra)'; + return 'DocumentInfo(tools: $tools, exportPresets: $exportPresets, extra: $extra)'; } @@ -48,11 +48,11 @@ abstract mixin class $DocumentInfoCopyWith<$Res> { factory $DocumentInfoCopyWith(DocumentInfo value, $Res Function(DocumentInfo) _then) = _$DocumentInfoCopyWithImpl; @useResult $Res call({ - List tools, List exportPresets, ViewOption view, Map extra + List tools, List exportPresets, Map extra }); -$ViewOptionCopyWith<$Res> get view; + } /// @nodoc @@ -65,25 +65,15 @@ class _$DocumentInfoCopyWithImpl<$Res> /// Create a copy of DocumentInfo /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? tools = null,Object? exportPresets = null,Object? view = null,Object? extra = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? tools = null,Object? exportPresets = null,Object? extra = null,}) { return _then(_self.copyWith( tools: null == tools ? _self.tools : tools // ignore: cast_nullable_to_non_nullable as List,exportPresets: null == exportPresets ? _self.exportPresets : exportPresets // ignore: cast_nullable_to_non_nullable -as List,view: null == view ? _self.view : view // ignore: cast_nullable_to_non_nullable -as ViewOption,extra: null == extra ? _self.extra : extra // ignore: cast_nullable_to_non_nullable +as List,extra: null == extra ? _self.extra : extra // ignore: cast_nullable_to_non_nullable as Map, )); } -/// Create a copy of DocumentInfo -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$ViewOptionCopyWith<$Res> get view { - - return $ViewOptionCopyWith<$Res>(_self.view, (value) { - return _then(_self.copyWith(view: value)); - }); -} + } @@ -92,7 +82,7 @@ $ViewOptionCopyWith<$Res> get view { @JsonSerializable() class _DocumentInfo implements DocumentInfo { - const _DocumentInfo({final List tools = const [], final List exportPresets = const [], this.view = const ViewOption(), final Map extra = const {}}): _tools = tools,_exportPresets = exportPresets,_extra = extra; + const _DocumentInfo({final List tools = const [], final List exportPresets = const [], final Map extra = const {}}): _tools = tools,_exportPresets = exportPresets,_extra = extra; factory _DocumentInfo.fromJson(Map json) => _$DocumentInfoFromJson(json); final List _tools; @@ -109,7 +99,6 @@ class _DocumentInfo implements DocumentInfo { return EqualUnmodifiableListView(_exportPresets); } -@override@JsonKey() final ViewOption view; final Map _extra; @override@JsonKey() Map get extra { if (_extra is EqualUnmodifiableMapView) return _extra; @@ -131,16 +120,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _DocumentInfo&&const DeepCollectionEquality().equals(other._tools, _tools)&&const DeepCollectionEquality().equals(other._exportPresets, _exportPresets)&&(identical(other.view, view) || other.view == view)&&const DeepCollectionEquality().equals(other._extra, _extra)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _DocumentInfo&&const DeepCollectionEquality().equals(other._tools, _tools)&&const DeepCollectionEquality().equals(other._exportPresets, _exportPresets)&&const DeepCollectionEquality().equals(other._extra, _extra)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_tools),const DeepCollectionEquality().hash(_exportPresets),view,const DeepCollectionEquality().hash(_extra)); +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_tools),const DeepCollectionEquality().hash(_exportPresets),const DeepCollectionEquality().hash(_extra)); @override String toString() { - return 'DocumentInfo(tools: $tools, exportPresets: $exportPresets, view: $view, extra: $extra)'; + return 'DocumentInfo(tools: $tools, exportPresets: $exportPresets, extra: $extra)'; } @@ -151,11 +140,11 @@ abstract mixin class _$DocumentInfoCopyWith<$Res> implements $DocumentInfoCopyWi factory _$DocumentInfoCopyWith(_DocumentInfo value, $Res Function(_DocumentInfo) _then) = __$DocumentInfoCopyWithImpl; @override @useResult $Res call({ - List tools, List exportPresets, ViewOption view, Map extra + List tools, List exportPresets, Map extra }); -@override $ViewOptionCopyWith<$Res> get view; + } /// @nodoc @@ -168,26 +157,16 @@ class __$DocumentInfoCopyWithImpl<$Res> /// Create a copy of DocumentInfo /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? tools = null,Object? exportPresets = null,Object? view = null,Object? extra = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? tools = null,Object? exportPresets = null,Object? extra = null,}) { return _then(_DocumentInfo( tools: null == tools ? _self._tools : tools // ignore: cast_nullable_to_non_nullable as List,exportPresets: null == exportPresets ? _self._exportPresets : exportPresets // ignore: cast_nullable_to_non_nullable -as List,view: null == view ? _self.view : view // ignore: cast_nullable_to_non_nullable -as ViewOption,extra: null == extra ? _self._extra : extra // ignore: cast_nullable_to_non_nullable +as List,extra: null == extra ? _self._extra : extra // ignore: cast_nullable_to_non_nullable as Map, )); } -/// Create a copy of DocumentInfo -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$ViewOptionCopyWith<$Res> get view { - - return $ViewOptionCopyWith<$Res>(_self.view, (value) { - return _then(_self.copyWith(view: value)); - }); -} + } // dart format on diff --git a/api/lib/src/models/info.g.dart b/api/lib/src/models/info.g.dart index fa2d144ceca7..c4439e6ca46f 100644 --- a/api/lib/src/models/info.g.dart +++ b/api/lib/src/models/info.g.dart @@ -19,9 +19,6 @@ _DocumentInfo _$DocumentInfoFromJson(Map json) => _DocumentInfo( ) .toList() ?? const [], - view: json['view'] == null - ? const ViewOption() - : ViewOption.fromJson(Map.from(json['view'] as Map)), extra: (json['extra'] as Map?)?.map((k, e) => MapEntry(k as String, e)) ?? const {}, @@ -31,6 +28,5 @@ Map _$DocumentInfoToJson(_DocumentInfo instance) => { 'tools': instance.tools.map((e) => e.toJson()).toList(), 'exportPresets': instance.exportPresets.map((e) => e.toJson()).toList(), - 'view': instance.view.toJson(), 'extra': instance.extra, }; diff --git a/api/lib/src/models/utilities.dart b/api/lib/src/models/utilities.dart deleted file mode 100644 index f25d0dd5cec7..000000000000 --- a/api/lib/src/models/utilities.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'utilities.freezed.dart'; -part 'utilities.g.dart'; - -@freezed -@JsonSerializable() -final class UtilitiesState with _$UtilitiesState { - @override - final bool lockCollection, lockLayer, lockZoom, lockHorizontal, lockVertical; - - const UtilitiesState({ - this.lockCollection = false, - this.lockLayer = false, - this.lockZoom = false, - this.lockHorizontal = false, - this.lockVertical = false, - }); - - factory UtilitiesState.fromJson(Map json) => - _$UtilitiesStateFromJson(json); - - Map toJson() => _$UtilitiesStateToJson(this); -} diff --git a/api/lib/src/models/utilities.freezed.dart b/api/lib/src/models/utilities.freezed.dart deleted file mode 100644 index f10f1894da62..000000000000 --- a/api/lib/src/models/utilities.freezed.dart +++ /dev/null @@ -1,81 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// coverage:ignore-file -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'utilities.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; - -/// @nodoc -mixin _$UtilitiesState { - - bool get lockCollection; bool get lockLayer; bool get lockZoom; bool get lockHorizontal; bool get lockVertical; -/// Create a copy of UtilitiesState -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$UtilitiesStateCopyWith get copyWith => _$UtilitiesStateCopyWithImpl(this as UtilitiesState, _$identity); - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is UtilitiesState&&(identical(other.lockCollection, lockCollection) || other.lockCollection == lockCollection)&&(identical(other.lockLayer, lockLayer) || other.lockLayer == lockLayer)&&(identical(other.lockZoom, lockZoom) || other.lockZoom == lockZoom)&&(identical(other.lockHorizontal, lockHorizontal) || other.lockHorizontal == lockHorizontal)&&(identical(other.lockVertical, lockVertical) || other.lockVertical == lockVertical)); -} - -@JsonKey(includeFromJson: false, includeToJson: false) -@override -int get hashCode => Object.hash(runtimeType,lockCollection,lockLayer,lockZoom,lockHorizontal,lockVertical); - -@override -String toString() { - return 'UtilitiesState(lockCollection: $lockCollection, lockLayer: $lockLayer, lockZoom: $lockZoom, lockHorizontal: $lockHorizontal, lockVertical: $lockVertical)'; -} - - -} - -/// @nodoc -abstract mixin class $UtilitiesStateCopyWith<$Res> { - factory $UtilitiesStateCopyWith(UtilitiesState value, $Res Function(UtilitiesState) _then) = _$UtilitiesStateCopyWithImpl; -@useResult -$Res call({ - bool lockCollection, bool lockLayer, bool lockZoom, bool lockHorizontal, bool lockVertical -}); - - - - -} -/// @nodoc -class _$UtilitiesStateCopyWithImpl<$Res> - implements $UtilitiesStateCopyWith<$Res> { - _$UtilitiesStateCopyWithImpl(this._self, this._then); - - final UtilitiesState _self; - final $Res Function(UtilitiesState) _then; - -/// Create a copy of UtilitiesState -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? lockCollection = null,Object? lockLayer = null,Object? lockZoom = null,Object? lockHorizontal = null,Object? lockVertical = null,}) { - return _then(UtilitiesState( -lockCollection: null == lockCollection ? _self.lockCollection : lockCollection // ignore: cast_nullable_to_non_nullable -as bool,lockLayer: null == lockLayer ? _self.lockLayer : lockLayer // ignore: cast_nullable_to_non_nullable -as bool,lockZoom: null == lockZoom ? _self.lockZoom : lockZoom // ignore: cast_nullable_to_non_nullable -as bool,lockHorizontal: null == lockHorizontal ? _self.lockHorizontal : lockHorizontal // ignore: cast_nullable_to_non_nullable -as bool,lockVertical: null == lockVertical ? _self.lockVertical : lockVertical // ignore: cast_nullable_to_non_nullable -as bool, - )); -} - -} - - - -// dart format on diff --git a/api/lib/src/models/utilities.g.dart b/api/lib/src/models/utilities.g.dart deleted file mode 100644 index 4dbad2ebd32a..000000000000 --- a/api/lib/src/models/utilities.g.dart +++ /dev/null @@ -1,24 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'utilities.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -UtilitiesState _$UtilitiesStateFromJson(Map json) => UtilitiesState( - lockCollection: json['lockCollection'] as bool? ?? false, - lockLayer: json['lockLayer'] as bool? ?? false, - lockZoom: json['lockZoom'] as bool? ?? false, - lockHorizontal: json['lockHorizontal'] as bool? ?? false, - lockVertical: json['lockVertical'] as bool? ?? false, -); - -Map _$UtilitiesStateToJson(UtilitiesState instance) => - { - 'lockCollection': instance.lockCollection, - 'lockLayer': instance.lockLayer, - 'lockZoom': instance.lockZoom, - 'lockHorizontal': instance.lockHorizontal, - 'lockVertical': instance.lockVertical, - }; diff --git a/api/lib/src/models/view.dart b/api/lib/src/models/view.dart deleted file mode 100644 index e73a6dede2bf..000000000000 --- a/api/lib/src/models/view.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'view.freezed.dart'; -part 'view.g.dart'; - -@freezed -@JsonSerializable() -final class ViewOption with _$ViewOption { - const ViewOption(); - - factory ViewOption.fromJson(Map json) => - _$ViewOptionFromJson(json); - - Map toJson() => _$ViewOptionToJson(this); -} diff --git a/api/lib/src/models/view.freezed.dart b/api/lib/src/models/view.freezed.dart deleted file mode 100644 index 58fe8e5b3243..000000000000 --- a/api/lib/src/models/view.freezed.dart +++ /dev/null @@ -1,46 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// coverage:ignore-file -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'view.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; - -/// @nodoc -mixin _$ViewOption { - - - - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ViewOption); -} - -@JsonKey(includeFromJson: false, includeToJson: false) -@override -int get hashCode => runtimeType.hashCode; - -@override -String toString() { - return 'ViewOption()'; -} - - -} - -/// @nodoc -class $ViewOptionCopyWith<$Res> { -$ViewOptionCopyWith(ViewOption _, $Res Function(ViewOption) __); -} - - - -// dart format on diff --git a/api/lib/src/models/view.g.dart b/api/lib/src/models/view.g.dart deleted file mode 100644 index fd37202e6925..000000000000 --- a/api/lib/src/models/view.g.dart +++ /dev/null @@ -1,12 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'view.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -ViewOption _$ViewOptionFromJson(Map json) => ViewOption(); - -Map _$ViewOptionToJson(ViewOption instance) => - {}; diff --git a/api/lib/src/protocol/event.dart b/api/lib/src/protocol/event.dart index 23912248c22c..97753bb26c78 100644 --- a/api/lib/src/protocol/event.dart +++ b/api/lib/src/protocol/event.dart @@ -59,11 +59,6 @@ sealed class DocumentEvent extends ReplayEvent with _$DocumentEvent { @Uint8ListJsonConverter() Uint8List data, ) = ThumbnailCaptured; - const factory DocumentEvent.viewChanged(ViewOption view) = ViewChanged; - - const factory DocumentEvent.utilitiesChanged(UtilitiesState state) = - UtilitiesChanged; - const factory DocumentEvent.elementsCreated( List elements, { @Uint8ListJsonConverter() @Default({}) Map assets, @@ -239,7 +234,6 @@ sealed class DocumentEvent extends ReplayEvent with _$DocumentEvent { CurrentCollectionChanged _ => false, CurrentAreaChanged _ => false, LayerVisibilityChanged _ => false, - UtilitiesChanged _ => false, PresentationModeEntered _ => false, PresentationModeExited _ => false, PresentationTick _ => false, diff --git a/api/lib/src/protocol/event.freezed.dart b/api/lib/src/protocol/event.freezed.dart index b532f553c51c..e517ac1341a9 100644 --- a/api/lib/src/protocol/event.freezed.dart +++ b/api/lib/src/protocol/event.freezed.dart @@ -372,14 +372,6 @@ DocumentEvent _$DocumentEventFromJson( case 'thumbnailCaptured': return ThumbnailCaptured.fromJson( json - ); - case 'viewChanged': - return ViewChanged.fromJson( - json - ); - case 'utilitiesChanged': - return UtilitiesChanged.fromJson( - json ); case 'elementsCreated': return ElementsCreated.fromJson( @@ -1073,170 +1065,6 @@ as Uint8List, /// @nodoc @JsonSerializable() -class ViewChanged extends DocumentEvent { - const ViewChanged(this.view, {final String? $type}): $type = $type ?? 'viewChanged',super._(); - factory ViewChanged.fromJson(Map json) => _$ViewChangedFromJson(json); - - final ViewOption view; - -@JsonKey(name: 'type') -final String $type; - - -/// Create a copy of DocumentEvent -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$ViewChangedCopyWith get copyWith => _$ViewChangedCopyWithImpl(this, _$identity); - -@override -Map toJson() { - return _$ViewChangedToJson(this, ); -} - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ViewChanged&&(identical(other.view, view) || other.view == view)); -} - -@JsonKey(includeFromJson: false, includeToJson: false) -@override -int get hashCode => Object.hash(runtimeType,view); - -@override -String toString() { - return 'DocumentEvent.viewChanged(view: $view)'; -} - - -} - -/// @nodoc -abstract mixin class $ViewChangedCopyWith<$Res> implements $DocumentEventCopyWith<$Res> { - factory $ViewChangedCopyWith(ViewChanged value, $Res Function(ViewChanged) _then) = _$ViewChangedCopyWithImpl; -@useResult -$Res call({ - ViewOption view -}); - - -$ViewOptionCopyWith<$Res> get view; - -} -/// @nodoc -class _$ViewChangedCopyWithImpl<$Res> - implements $ViewChangedCopyWith<$Res> { - _$ViewChangedCopyWithImpl(this._self, this._then); - - final ViewChanged _self; - final $Res Function(ViewChanged) _then; - -/// Create a copy of DocumentEvent -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') $Res call({Object? view = null,}) { - return _then(ViewChanged( -null == view ? _self.view : view // ignore: cast_nullable_to_non_nullable -as ViewOption, - )); -} - -/// Create a copy of DocumentEvent -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$ViewOptionCopyWith<$Res> get view { - - return $ViewOptionCopyWith<$Res>(_self.view, (value) { - return _then(_self.copyWith(view: value)); - }); -} -} - -/// @nodoc -@JsonSerializable() - -class UtilitiesChanged extends DocumentEvent { - const UtilitiesChanged(this.state, {final String? $type}): $type = $type ?? 'utilitiesChanged',super._(); - factory UtilitiesChanged.fromJson(Map json) => _$UtilitiesChangedFromJson(json); - - final UtilitiesState state; - -@JsonKey(name: 'type') -final String $type; - - -/// Create a copy of DocumentEvent -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$UtilitiesChangedCopyWith get copyWith => _$UtilitiesChangedCopyWithImpl(this, _$identity); - -@override -Map toJson() { - return _$UtilitiesChangedToJson(this, ); -} - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is UtilitiesChanged&&(identical(other.state, state) || other.state == state)); -} - -@JsonKey(includeFromJson: false, includeToJson: false) -@override -int get hashCode => Object.hash(runtimeType,state); - -@override -String toString() { - return 'DocumentEvent.utilitiesChanged(state: $state)'; -} - - -} - -/// @nodoc -abstract mixin class $UtilitiesChangedCopyWith<$Res> implements $DocumentEventCopyWith<$Res> { - factory $UtilitiesChangedCopyWith(UtilitiesChanged value, $Res Function(UtilitiesChanged) _then) = _$UtilitiesChangedCopyWithImpl; -@useResult -$Res call({ - UtilitiesState state -}); - - -$UtilitiesStateCopyWith<$Res> get state; - -} -/// @nodoc -class _$UtilitiesChangedCopyWithImpl<$Res> - implements $UtilitiesChangedCopyWith<$Res> { - _$UtilitiesChangedCopyWithImpl(this._self, this._then); - - final UtilitiesChanged _self; - final $Res Function(UtilitiesChanged) _then; - -/// Create a copy of DocumentEvent -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') $Res call({Object? state = null,}) { - return _then(UtilitiesChanged( -null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable -as UtilitiesState, - )); -} - -/// Create a copy of DocumentEvent -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$UtilitiesStateCopyWith<$Res> get state { - - return $UtilitiesStateCopyWith<$Res>(_self.state, (value) { - return _then(_self.copyWith(state: value)); - }); -} -} - -/// @nodoc -@JsonSerializable() - class ElementsCreated extends DocumentEvent { const ElementsCreated(final List elements, {@Uint8ListJsonConverter() final Map assets = const {}, final String? $type}): _elements = elements,_assets = assets,$type = $type ?? 'elementsCreated',super._(); factory ElementsCreated.fromJson(Map json) => _$ElementsCreatedFromJson(json); diff --git a/api/lib/src/protocol/event.g.dart b/api/lib/src/protocol/event.g.dart index d57a4958e911..c39bb7040cea 100644 --- a/api/lib/src/protocol/event.g.dart +++ b/api/lib/src/protocol/event.g.dart @@ -107,22 +107,6 @@ Map _$ThumbnailCapturedToJson(ThumbnailCaptured instance) => 'type': instance.$type, }; -ViewChanged _$ViewChangedFromJson(Map json) => ViewChanged( - ViewOption.fromJson(Map.from(json['view'] as Map)), - $type: json['type'] as String?, -); - -Map _$ViewChangedToJson(ViewChanged instance) => - {'view': instance.view.toJson(), 'type': instance.$type}; - -UtilitiesChanged _$UtilitiesChangedFromJson(Map json) => UtilitiesChanged( - UtilitiesState.fromJson(Map.from(json['state'] as Map)), - $type: json['type'] as String?, -); - -Map _$UtilitiesChangedToJson(UtilitiesChanged instance) => - {'state': instance.state.toJson(), 'type': instance.$type}; - ElementsCreated _$ElementsCreatedFromJson(Map json) => ElementsCreated( (json['elements'] as List) .map((e) => PadElement.fromJson(Map.from(e as Map))) diff --git a/app/lib/bloc/document_state.dart b/app/lib/bloc/document_state.dart index 3dcbd08f9907..42ea2b657d86 100644 --- a/app/lib/bloc/document_state.dart +++ b/app/lib/bloc/document_state.dart @@ -20,10 +20,9 @@ abstract class DocumentState { String? get currentLayer => null; - Future saveData([NoteData? current, ViewOption? viewOption]) => - Future.value(data); - Future saveBytes([NoteData? current, ViewOption? viewOption]) => - saveData(current, viewOption).then((e) => e?.exportAsBytes()); + Future saveData([NoteData? current]) => Future.value(data); + Future saveBytes([NoteData? current]) => + saveData(current).then((e) => e?.exportAsBytes()); } class DocumentLoadInProgress extends DocumentState { @@ -67,9 +66,6 @@ abstract class DocumentLoaded extends DocumentState { current.setRawPage(await compute(_encodePage, page), pageName).$1; NoteData _updateMetadata(NoteData current) => current.setMetadata(metadata.copyWith(updatedAt: DateTime.now().toUtc())); - NoteData _updateInfo(NoteData current, ViewOption viewOption) => - current.setInfo(info.copyWith(view: viewOption)); - DocumentLoaded( this.data, { DocumentPage? page, @@ -92,18 +88,17 @@ abstract class DocumentLoaded extends DocumentState { Area? get currentArea => null; @override - Future saveData([NoteData? current, ViewOption? viewOption]) async { + Future saveData([NoteData? current]) async { current ??= data; - viewOption ??= info.view; current = await _updatePage(current); current = _updateMetadata(current); - current = _updateInfo(current, viewOption); + current = current.setInfo(info); return current; } @override - Future saveBytes([NoteData? current, ViewOption? viewOption]) => - saveData(current, viewOption).then((e) => e.exportAsBytes()); + Future saveBytes([NoteData? current]) => + saveData(current).then((e) => e.exportAsBytes()); } class DocumentLoadSuccess extends DocumentLoaded { diff --git a/app/lib/cubits/editor_controller.dart b/app/lib/cubits/editor_controller.dart index b1c23d64de05..a132171d7c6e 100644 --- a/app/lib/cubits/editor_controller.dart +++ b/app/lib/cubits/editor_controller.dart @@ -3,6 +3,7 @@ import 'package:butterfly/cubits/editor_session.dart'; import 'package:butterfly/cubits/editor_runtime.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/transform.dart'; +import 'package:butterfly/models/persisted_document_state.dart'; import 'package:butterfly/renderers/cursors/user.dart'; import 'package:butterfly/renderers/renderer.dart'; import 'package:butterfly/services/network.dart'; @@ -80,18 +81,18 @@ class EditorController implements EditorRuntimeContext { viewCubit = EditorViewCubit( editorSessionCubit: editorSessionCubit, initial: EditorViewState( - utilities: - editorSessionCubit?.state.utilities ?? const UtilitiesState(), + locks: + editorSessionCubit?.state.locks ?? const PersistentLockState(), navigatorEnabled: - editorSessionCubit?.state.navigatorEnabled ?? false, + editorSessionCubit?.state.navigator.enabled ?? false, navigatorPage: editorSessionCubit?.navigatorPage ?? NavigatorPage.waypoints, areaNavigatorCreate: - editorSessionCubit?.state.areaNavigatorCreate ?? true, + editorSessionCubit?.state.areaNavigator.create ?? true, areaNavigatorExact: - editorSessionCubit?.state.areaNavigatorExact ?? true, + editorSessionCubit?.state.areaNavigator.exact ?? true, areaNavigatorAsk: - editorSessionCubit?.state.areaNavigatorAsk ?? false, + editorSessionCubit?.state.areaNavigator.ask ?? false, ), ) { rendererCubit.bindController(this); diff --git a/app/lib/cubits/editor_runtime.dart b/app/lib/cubits/editor_runtime.dart index d71cf45f67fa..bc5db14128fd 100644 --- a/app/lib/cubits/editor_runtime.dart +++ b/app/lib/cubits/editor_runtime.dart @@ -17,6 +17,7 @@ import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/services/network.dart'; import 'package:butterfly/services/logger.dart'; import 'package:butterfly/helpers/xml.dart'; +import 'package:butterfly/models/persisted_document_state.dart'; import 'package:butterfly/view_painter.dart'; import 'package:butterfly/views/navigator/view.dart'; import 'package:butterfly_api/butterfly_api.dart'; diff --git a/app/lib/cubits/editor_runtime.freezed.dart b/app/lib/cubits/editor_runtime.freezed.dart index 586926c2c032..ee2e3b9be9f3 100644 --- a/app/lib/cubits/editor_runtime.freezed.dart +++ b/app/lib/cubits/editor_runtime.freezed.dart @@ -694,7 +694,7 @@ as bool, /// @nodoc mixin _$EditorViewState implements DiagnosticableTreeMixin { - UtilitiesState get utilities; ViewOption get viewOption; bool get areaNavigatorCreate; bool get areaNavigatorExact; bool get areaNavigatorAsk; bool get navigatorEnabled; NavigatorPage get navigatorPage; String get userName; + PersistentLockState get locks; bool get areaNavigatorCreate; bool get areaNavigatorExact; bool get areaNavigatorAsk; bool get navigatorEnabled; NavigatorPage get navigatorPage; String get userName; /// Create a copy of EditorViewState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -706,21 +706,21 @@ $EditorViewStateCopyWith get copyWith => _$EditorViewStateCopyW void debugFillProperties(DiagnosticPropertiesBuilder properties) { properties ..add(DiagnosticsProperty('type', 'EditorViewState')) - ..add(DiagnosticsProperty('utilities', utilities))..add(DiagnosticsProperty('viewOption', viewOption))..add(DiagnosticsProperty('areaNavigatorCreate', areaNavigatorCreate))..add(DiagnosticsProperty('areaNavigatorExact', areaNavigatorExact))..add(DiagnosticsProperty('areaNavigatorAsk', areaNavigatorAsk))..add(DiagnosticsProperty('navigatorEnabled', navigatorEnabled))..add(DiagnosticsProperty('navigatorPage', navigatorPage))..add(DiagnosticsProperty('userName', userName)); + ..add(DiagnosticsProperty('locks', locks))..add(DiagnosticsProperty('areaNavigatorCreate', areaNavigatorCreate))..add(DiagnosticsProperty('areaNavigatorExact', areaNavigatorExact))..add(DiagnosticsProperty('areaNavigatorAsk', areaNavigatorAsk))..add(DiagnosticsProperty('navigatorEnabled', navigatorEnabled))..add(DiagnosticsProperty('navigatorPage', navigatorPage))..add(DiagnosticsProperty('userName', userName)); } @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is EditorViewState&&(identical(other.utilities, utilities) || other.utilities == utilities)&&(identical(other.viewOption, viewOption) || other.viewOption == viewOption)&&(identical(other.areaNavigatorCreate, areaNavigatorCreate) || other.areaNavigatorCreate == areaNavigatorCreate)&&(identical(other.areaNavigatorExact, areaNavigatorExact) || other.areaNavigatorExact == areaNavigatorExact)&&(identical(other.areaNavigatorAsk, areaNavigatorAsk) || other.areaNavigatorAsk == areaNavigatorAsk)&&(identical(other.navigatorEnabled, navigatorEnabled) || other.navigatorEnabled == navigatorEnabled)&&(identical(other.navigatorPage, navigatorPage) || other.navigatorPage == navigatorPage)&&(identical(other.userName, userName) || other.userName == userName)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is EditorViewState&&(identical(other.locks, locks) || other.locks == locks)&&(identical(other.areaNavigatorCreate, areaNavigatorCreate) || other.areaNavigatorCreate == areaNavigatorCreate)&&(identical(other.areaNavigatorExact, areaNavigatorExact) || other.areaNavigatorExact == areaNavigatorExact)&&(identical(other.areaNavigatorAsk, areaNavigatorAsk) || other.areaNavigatorAsk == areaNavigatorAsk)&&(identical(other.navigatorEnabled, navigatorEnabled) || other.navigatorEnabled == navigatorEnabled)&&(identical(other.navigatorPage, navigatorPage) || other.navigatorPage == navigatorPage)&&(identical(other.userName, userName) || other.userName == userName)); } @override -int get hashCode => Object.hash(runtimeType,utilities,viewOption,areaNavigatorCreate,areaNavigatorExact,areaNavigatorAsk,navigatorEnabled,navigatorPage,userName); +int get hashCode => Object.hash(runtimeType,locks,areaNavigatorCreate,areaNavigatorExact,areaNavigatorAsk,navigatorEnabled,navigatorPage,userName); @override String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'EditorViewState(utilities: $utilities, viewOption: $viewOption, areaNavigatorCreate: $areaNavigatorCreate, areaNavigatorExact: $areaNavigatorExact, areaNavigatorAsk: $areaNavigatorAsk, navigatorEnabled: $navigatorEnabled, navigatorPage: $navigatorPage, userName: $userName)'; + return 'EditorViewState(locks: $locks, areaNavigatorCreate: $areaNavigatorCreate, areaNavigatorExact: $areaNavigatorExact, areaNavigatorAsk: $areaNavigatorAsk, navigatorEnabled: $navigatorEnabled, navigatorPage: $navigatorPage, userName: $userName)'; } @@ -731,11 +731,11 @@ abstract mixin class $EditorViewStateCopyWith<$Res> { factory $EditorViewStateCopyWith(EditorViewState value, $Res Function(EditorViewState) _then) = _$EditorViewStateCopyWithImpl; @useResult $Res call({ - UtilitiesState utilities, ViewOption viewOption, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, bool navigatorEnabled, NavigatorPage navigatorPage, String userName + PersistentLockState locks, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, bool navigatorEnabled, NavigatorPage navigatorPage, String userName }); -$UtilitiesStateCopyWith<$Res> get utilities;$ViewOptionCopyWith<$Res> get viewOption; +$PersistentLockStateCopyWith<$Res> get locks; } /// @nodoc @@ -748,11 +748,10 @@ class _$EditorViewStateCopyWithImpl<$Res> /// Create a copy of EditorViewState /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? utilities = null,Object? viewOption = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? userName = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? locks = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? userName = null,}) { return _then(_self.copyWith( -utilities: null == utilities ? _self.utilities : utilities // ignore: cast_nullable_to_non_nullable -as UtilitiesState,viewOption: null == viewOption ? _self.viewOption : viewOption // ignore: cast_nullable_to_non_nullable -as ViewOption,areaNavigatorCreate: null == areaNavigatorCreate ? _self.areaNavigatorCreate : areaNavigatorCreate // ignore: cast_nullable_to_non_nullable +locks: null == locks ? _self.locks : locks // ignore: cast_nullable_to_non_nullable +as PersistentLockState,areaNavigatorCreate: null == areaNavigatorCreate ? _self.areaNavigatorCreate : areaNavigatorCreate // ignore: cast_nullable_to_non_nullable as bool,areaNavigatorExact: null == areaNavigatorExact ? _self.areaNavigatorExact : areaNavigatorExact // ignore: cast_nullable_to_non_nullable as bool,areaNavigatorAsk: null == areaNavigatorAsk ? _self.areaNavigatorAsk : areaNavigatorAsk // ignore: cast_nullable_to_non_nullable as bool,navigatorEnabled: null == navigatorEnabled ? _self.navigatorEnabled : navigatorEnabled // ignore: cast_nullable_to_non_nullable @@ -765,19 +764,10 @@ as String, /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') -$UtilitiesStateCopyWith<$Res> get utilities { +$PersistentLockStateCopyWith<$Res> get locks { - return $UtilitiesStateCopyWith<$Res>(_self.utilities, (value) { - return _then(_self.copyWith(utilities: value)); - }); -}/// Create a copy of EditorViewState -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$ViewOptionCopyWith<$Res> get viewOption { - - return $ViewOptionCopyWith<$Res>(_self.viewOption, (value) { - return _then(_self.copyWith(viewOption: value)); + return $PersistentLockStateCopyWith<$Res>(_self.locks, (value) { + return _then(_self.copyWith(locks: value)); }); } } @@ -788,11 +778,10 @@ $ViewOptionCopyWith<$Res> get viewOption { class _EditorViewState with DiagnosticableTreeMixin implements EditorViewState { - const _EditorViewState({this.utilities = const UtilitiesState(), this.viewOption = const ViewOption(), this.areaNavigatorCreate = true, this.areaNavigatorExact = true, this.areaNavigatorAsk = false, this.navigatorEnabled = false, this.navigatorPage = NavigatorPage.waypoints, this.userName = ''}); + const _EditorViewState({this.locks = const PersistentLockState(), this.areaNavigatorCreate = true, this.areaNavigatorExact = true, this.areaNavigatorAsk = false, this.navigatorEnabled = false, this.navigatorPage = NavigatorPage.waypoints, this.userName = ''}); -@override@JsonKey() final UtilitiesState utilities; -@override@JsonKey() final ViewOption viewOption; +@override@JsonKey() final PersistentLockState locks; @override@JsonKey() final bool areaNavigatorCreate; @override@JsonKey() final bool areaNavigatorExact; @override@JsonKey() final bool areaNavigatorAsk; @@ -811,21 +800,21 @@ _$EditorViewStateCopyWith<_EditorViewState> get copyWith => __$EditorViewStateCo void debugFillProperties(DiagnosticPropertiesBuilder properties) { properties ..add(DiagnosticsProperty('type', 'EditorViewState')) - ..add(DiagnosticsProperty('utilities', utilities))..add(DiagnosticsProperty('viewOption', viewOption))..add(DiagnosticsProperty('areaNavigatorCreate', areaNavigatorCreate))..add(DiagnosticsProperty('areaNavigatorExact', areaNavigatorExact))..add(DiagnosticsProperty('areaNavigatorAsk', areaNavigatorAsk))..add(DiagnosticsProperty('navigatorEnabled', navigatorEnabled))..add(DiagnosticsProperty('navigatorPage', navigatorPage))..add(DiagnosticsProperty('userName', userName)); + ..add(DiagnosticsProperty('locks', locks))..add(DiagnosticsProperty('areaNavigatorCreate', areaNavigatorCreate))..add(DiagnosticsProperty('areaNavigatorExact', areaNavigatorExact))..add(DiagnosticsProperty('areaNavigatorAsk', areaNavigatorAsk))..add(DiagnosticsProperty('navigatorEnabled', navigatorEnabled))..add(DiagnosticsProperty('navigatorPage', navigatorPage))..add(DiagnosticsProperty('userName', userName)); } @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _EditorViewState&&(identical(other.utilities, utilities) || other.utilities == utilities)&&(identical(other.viewOption, viewOption) || other.viewOption == viewOption)&&(identical(other.areaNavigatorCreate, areaNavigatorCreate) || other.areaNavigatorCreate == areaNavigatorCreate)&&(identical(other.areaNavigatorExact, areaNavigatorExact) || other.areaNavigatorExact == areaNavigatorExact)&&(identical(other.areaNavigatorAsk, areaNavigatorAsk) || other.areaNavigatorAsk == areaNavigatorAsk)&&(identical(other.navigatorEnabled, navigatorEnabled) || other.navigatorEnabled == navigatorEnabled)&&(identical(other.navigatorPage, navigatorPage) || other.navigatorPage == navigatorPage)&&(identical(other.userName, userName) || other.userName == userName)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _EditorViewState&&(identical(other.locks, locks) || other.locks == locks)&&(identical(other.areaNavigatorCreate, areaNavigatorCreate) || other.areaNavigatorCreate == areaNavigatorCreate)&&(identical(other.areaNavigatorExact, areaNavigatorExact) || other.areaNavigatorExact == areaNavigatorExact)&&(identical(other.areaNavigatorAsk, areaNavigatorAsk) || other.areaNavigatorAsk == areaNavigatorAsk)&&(identical(other.navigatorEnabled, navigatorEnabled) || other.navigatorEnabled == navigatorEnabled)&&(identical(other.navigatorPage, navigatorPage) || other.navigatorPage == navigatorPage)&&(identical(other.userName, userName) || other.userName == userName)); } @override -int get hashCode => Object.hash(runtimeType,utilities,viewOption,areaNavigatorCreate,areaNavigatorExact,areaNavigatorAsk,navigatorEnabled,navigatorPage,userName); +int get hashCode => Object.hash(runtimeType,locks,areaNavigatorCreate,areaNavigatorExact,areaNavigatorAsk,navigatorEnabled,navigatorPage,userName); @override String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'EditorViewState(utilities: $utilities, viewOption: $viewOption, areaNavigatorCreate: $areaNavigatorCreate, areaNavigatorExact: $areaNavigatorExact, areaNavigatorAsk: $areaNavigatorAsk, navigatorEnabled: $navigatorEnabled, navigatorPage: $navigatorPage, userName: $userName)'; + return 'EditorViewState(locks: $locks, areaNavigatorCreate: $areaNavigatorCreate, areaNavigatorExact: $areaNavigatorExact, areaNavigatorAsk: $areaNavigatorAsk, navigatorEnabled: $navigatorEnabled, navigatorPage: $navigatorPage, userName: $userName)'; } @@ -836,11 +825,11 @@ abstract mixin class _$EditorViewStateCopyWith<$Res> implements $EditorViewState factory _$EditorViewStateCopyWith(_EditorViewState value, $Res Function(_EditorViewState) _then) = __$EditorViewStateCopyWithImpl; @override @useResult $Res call({ - UtilitiesState utilities, ViewOption viewOption, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, bool navigatorEnabled, NavigatorPage navigatorPage, String userName + PersistentLockState locks, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, bool navigatorEnabled, NavigatorPage navigatorPage, String userName }); -@override $UtilitiesStateCopyWith<$Res> get utilities;@override $ViewOptionCopyWith<$Res> get viewOption; +@override $PersistentLockStateCopyWith<$Res> get locks; } /// @nodoc @@ -853,11 +842,10 @@ class __$EditorViewStateCopyWithImpl<$Res> /// Create a copy of EditorViewState /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? utilities = null,Object? viewOption = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? userName = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? locks = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? userName = null,}) { return _then(_EditorViewState( -utilities: null == utilities ? _self.utilities : utilities // ignore: cast_nullable_to_non_nullable -as UtilitiesState,viewOption: null == viewOption ? _self.viewOption : viewOption // ignore: cast_nullable_to_non_nullable -as ViewOption,areaNavigatorCreate: null == areaNavigatorCreate ? _self.areaNavigatorCreate : areaNavigatorCreate // ignore: cast_nullable_to_non_nullable +locks: null == locks ? _self.locks : locks // ignore: cast_nullable_to_non_nullable +as PersistentLockState,areaNavigatorCreate: null == areaNavigatorCreate ? _self.areaNavigatorCreate : areaNavigatorCreate // ignore: cast_nullable_to_non_nullable as bool,areaNavigatorExact: null == areaNavigatorExact ? _self.areaNavigatorExact : areaNavigatorExact // ignore: cast_nullable_to_non_nullable as bool,areaNavigatorAsk: null == areaNavigatorAsk ? _self.areaNavigatorAsk : areaNavigatorAsk // ignore: cast_nullable_to_non_nullable as bool,navigatorEnabled: null == navigatorEnabled ? _self.navigatorEnabled : navigatorEnabled // ignore: cast_nullable_to_non_nullable @@ -871,19 +859,10 @@ as String, /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') -$UtilitiesStateCopyWith<$Res> get utilities { - - return $UtilitiesStateCopyWith<$Res>(_self.utilities, (value) { - return _then(_self.copyWith(utilities: value)); - }); -}/// Create a copy of EditorViewState -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$ViewOptionCopyWith<$Res> get viewOption { +$PersistentLockStateCopyWith<$Res> get locks { - return $ViewOptionCopyWith<$Res>(_self.viewOption, (value) { - return _then(_self.copyWith(viewOption: value)); + return $PersistentLockStateCopyWith<$Res>(_self.locks, (value) { + return _then(_self.copyWith(locks: value)); }); } } diff --git a/app/lib/cubits/editor_session.dart b/app/lib/cubits/editor_session.dart index 6a5509d5d30f..c4e465b4267c 100644 --- a/app/lib/cubits/editor_session.dart +++ b/app/lib/cubits/editor_session.dart @@ -1,8 +1,8 @@ import 'dart:async'; -import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/models/persisted_document_state.dart'; +import 'package:butterfly/repositories/document_state.dart'; import 'package:butterfly/views/navigator/view.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:collection/collection.dart'; @@ -11,7 +11,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class EditorSessionCubit extends Cubit { EditorSessionCubit({ - required this.fileSystem, + required this.repository, required TransformCubit transformCubit, required PersistedDocumentState initialState, required this.pathKey, @@ -22,7 +22,7 @@ class EditorSessionCubit extends Cubit { _transformSubscription = transformCubit.stream.listen(_onTransformChanged); } - final DocumentStateFileSystem fileSystem; + final DocumentStateRepository repository; final TransformCubit _transformCubit; final String? pathKey; final String? contentHash; @@ -30,31 +30,12 @@ class EditorSessionCubit extends Cubit { StreamSubscription? _transformSubscription; Timer? _saveDebounce; - static Future load({ - required DocumentStateFileSystem fileSystem, - String? contentHash, - String? pathKey, - bool allowContentHash = true, - }) async { - await fileSystem.initialize(); - if (allowContentHash && contentHash != null) { - final byContent = await fileSystem.getFile( - documentStateContentKey(contentHash), - ); - if (byContent != null) return byContent; - } - if (pathKey != null) { - return fileSystem.getFile(pathKey); - } - return null; - } - static PersistedDocumentState buildInitial({ PersistedDocumentState? restored, required NoteData document, required DocumentPage page, required String? fallbackPageName, - required UtilitiesState fallbackUtilities, + required PersistentLockState fallbackLocks, String? pathKey, String? contentHash, }) { @@ -65,23 +46,24 @@ class EditorSessionCubit extends Cubit { : fallbackPageName; final layers = page.layers.map((e) => e.id).nonNulls.toSet(); final currentLayer = - restored?.currentLayer != null && - restored!.currentLayer.isNotEmpty && - layers.contains(restored.currentLayer) - ? restored.currentLayer + restored?.layers.currentLayer != null && + restored!.layers.currentLayer.isNotEmpty && + layers.contains(restored.layers.currentLayer) + ? restored.layers.currentLayer : page.layers.lastOrNull?.id ?? ''; final invisibleLayers = - restored?.invisibleLayers.where(layers.contains).toSet() ?? + restored?.layers.invisibleLayers.where(layers.contains).toSet() ?? const {}; - return (restored ?? PersistedDocumentState(utilities: fallbackUtilities)) - .copyWith( - pathKey: pathKey, - contentHash: contentHash, - pageName: pageName, - currentLayer: currentLayer, - invisibleLayers: invisibleLayers, - updatedAt: restored?.updatedAt, - ); + return (restored ?? PersistedDocumentState(locks: fallbackLocks)).copyWith( + pathKey: pathKey, + contentHash: contentHash, + pageName: pageName, + layers: (restored?.layers ?? const PersistedLayerState()).copyWith( + currentLayer: currentLayer, + invisibleLayers: invisibleLayers, + ), + updatedAt: restored?.updatedAt, + ); } CameraTransform get cameraTransform => CameraTransform( @@ -105,7 +87,7 @@ class EditorSessionCubit extends Cubit { NavigatorPage get navigatorPage => NavigatorPage.values.firstWhereOrNull( - (e) => e.name == state.navigatorPage, + (e) => e.name == state.navigator.page, ) ?? NavigatorPage.waypoints; @@ -126,9 +108,9 @@ class EditorSessionCubit extends Cubit { unawaited(saveNow()); } - void updateUtilities(UtilitiesState utilities) { - if (state.utilities == utilities) return; - emit(state.copyWith(utilities: utilities)); + void updateLocks(PersistentLockState locks) { + if (state.locks == locks) return; + emit(state.copyWith(locks: locks)); unawaited(saveNow()); } @@ -144,8 +126,10 @@ class EditorSessionCubit extends Cubit { void updateNavigator({bool? enabled, NavigatorPage? page}) { final next = state.copyWith( - navigatorEnabled: enabled ?? state.navigatorEnabled, - navigatorPage: page?.name ?? state.navigatorPage, + navigator: state.navigator.copyWith( + enabled: enabled ?? state.navigator.enabled, + page: page?.name ?? state.navigator.page, + ), ); if (next == state) return; emit(next); @@ -158,9 +142,11 @@ class EditorSessionCubit extends Cubit { Set? invisibleLayers, }) { final next = state.copyWith( - currentLayer: currentLayer ?? state.currentLayer, - currentCollection: currentCollection ?? state.currentCollection, - invisibleLayers: invisibleLayers ?? state.invisibleLayers, + layers: state.layers.copyWith( + currentLayer: currentLayer ?? state.layers.currentLayer, + currentCollection: currentCollection ?? state.layers.currentCollection, + invisibleLayers: invisibleLayers ?? state.layers.invisibleLayers, + ), ); if (next == state) return; emit(next); @@ -169,9 +155,11 @@ class EditorSessionCubit extends Cubit { void updateAreaNavigator({bool? create, bool? exact, bool? ask}) { final next = state.copyWith( - areaNavigatorCreate: create ?? state.areaNavigatorCreate, - areaNavigatorExact: exact ?? state.areaNavigatorExact, - areaNavigatorAsk: ask ?? state.areaNavigatorAsk, + areaNavigator: state.areaNavigator.copyWith( + create: create ?? state.areaNavigator.create, + exact: exact ?? state.areaNavigator.exact, + ask: ask ?? state.areaNavigator.ask, + ), ); if (next == state) return; emit(next); @@ -191,22 +179,7 @@ class EditorSessionCubit extends Cubit { _saveDebounce = null; final next = state.touch(pathKey: pathKey, contentHash: contentHash); emit(next); - await fileSystem.initialize(); - if (contentHash != null) { - final key = documentStateContentKey(contentHash!); - if (await fileSystem.hasKey(key)) { - await fileSystem.updateFile(key, next); - } else { - await fileSystem.createFile(key, next); - } - } - if (pathKey != null) { - if (await fileSystem.hasKey(pathKey!)) { - await fileSystem.updateFile(pathKey!, next); - } else { - await fileSystem.createFile(pathKey!, next); - } - } + await repository.save(next, contentHash: contentHash, pathKey: pathKey); } @override diff --git a/app/lib/cubits/editor_view.dart b/app/lib/cubits/editor_view.dart index a8611108a838..177b57bd1bad 100644 --- a/app/lib/cubits/editor_view.dart +++ b/app/lib/cubits/editor_view.dart @@ -3,8 +3,7 @@ part of 'editor_runtime.dart'; @freezed sealed class EditorViewState with _$EditorViewState { const factory EditorViewState({ - @Default(UtilitiesState()) UtilitiesState utilities, - @Default(ViewOption()) ViewOption viewOption, + @Default(PersistentLockState()) PersistentLockState locks, @Default(true) bool areaNavigatorCreate, @Default(true) bool areaNavigatorExact, @Default(false) bool areaNavigatorAsk, @@ -35,15 +34,10 @@ class EditorViewCubit extends Cubit { void replace(EditorViewState state) => emit(state); - void updateUtilities({UtilitiesState? utilities, ViewOption? view}) { - emit( - state.copyWith( - utilities: utilities ?? state.utilities, - viewOption: view ?? state.viewOption, - ), - ); - if (utilities != null) { - editorSessionCubit?.updateUtilities(utilities); + void updateLocks({PersistentLockState? locks}) { + emit(state.copyWith(locks: locks ?? state.locks)); + if (locks != null) { + editorSessionCubit?.updateLocks(locks); } } diff --git a/app/lib/cubits/settings.dart b/app/lib/cubits/settings.dart index cdcafae2d715..9d113a73956a 100644 --- a/app/lib/cubits/settings.dart +++ b/app/lib/cubits/settings.dart @@ -495,7 +495,6 @@ sealed class ButterflySettings with _$ButterflySettings, LeapSettings { @Default(true) bool delayedAutosave, @Default(3) int autosaveDelaySeconds, @Default(false) bool hideCursorWhileDrawing, - @Default(UtilitiesState()) UtilitiesState utilities, @Default(StartupBehavior.openHomeScreen) StartupBehavior onStartup, @Default(SimpleToolbarVisibility.show) SimpleToolbarVisibility simpleToolbarVisibility, @@ -679,11 +678,6 @@ sealed class ButterflySettings with _$ButterflySettings, LeapSettings { NavigatorPosition.left, ) : NavigatorPosition.left, - utilities: prefs.containsKey('utilities') - ? UtilitiesState.fromJson( - _decodeJsonMapOrEmpty(prefs.getString('utilities')), - ) - : const UtilitiesState(), onStartup: prefs.containsKey('on_startup') ? _enumByNameOr( StartupBehavior.values, @@ -844,7 +838,6 @@ sealed class ButterflySettings with _$ButterflySettings, LeapSettings { await prefs.setInt('toolbar_rows', toolbarRows); await prefs.setBool('hide_cursor_while_drawing', hideCursorWhileDrawing); await prefs.setString('navigator_position', navigatorPosition.name); - await prefs.setString('utilities', json.encode(utilities.toJson())); await prefs.setString('on_startup', onStartup.name); await prefs.setString( 'simple_toolbar_visibility', @@ -1513,11 +1506,6 @@ class SettingsCubit extends Cubit return save(); } - Future changeUtilities(UtilitiesState utilities) { - emit(state.copyWith(utilities: utilities)); - return save(); - } - Future changeStartupBehavior(StartupBehavior behavior) { emit(state.copyWith(onStartup: behavior)); return save(); diff --git a/app/lib/cubits/settings.freezed.dart b/app/lib/cubits/settings.freezed.dart index 4ed766008c18..f91c69a59745 100644 --- a/app/lib/cubits/settings.freezed.dart +++ b/app/lib/cubits/settings.freezed.dart @@ -537,7 +537,7 @@ as String?, /// @nodoc mixin _$ButterflySettings implements DiagnosticableTreeMixin { - ThemeMode get theme; ThemeDensity get density; double? get limitViewportMultiplier; bool get limitViewportPositive; String get localeTag; String get documentPath; double get gestureSensitivity; double get touchSensitivity; double get selectSensitivity; double get scrollSensitivity; bool? get penOnlyInput; bool get showPenOnlyToggle; bool get inputGestures; String get design; BannerVisibility get bannerVisibility;@JsonKey(includeFromJson: false, includeToJson: false) List get history; bool get zoomEnabled; ZoomPosition get zoomPosition; ZoomPosition get propertyPosition; String? get lastVersion;@JsonKey(includeFromJson: false, includeToJson: false) List get connections; String get defaultRemote; bool get nativeTitleBar; bool get startInFullScreen; bool get navigationRail; IgnorePressure get ignorePressure; SyncMode get syncMode; InputConfiguration get inputConfiguration; String get fallbackPack; List get starred; List get favoriteTemplates; String get defaultTemplate; NavigatorPosition get navigatorPosition; ToolbarPosition get toolbarPosition; ToolbarSize get toolbarSize; SortBy get sortBy; SortOrder get sortOrder; double get imageScale; PlatformTheme get platformTheme;@SRGBConverter() List get recentColors; List get flags; bool get spreadPages; bool get highContrast; bool get gridView; bool get hideExtension; bool get autosave; bool get showSaveButton; int get toolbarRows; bool get delayedAutosave; int get autosaveDelaySeconds; bool get hideCursorWhileDrawing; UtilitiesState get utilities; StartupBehavior get onStartup; SimpleToolbarVisibility get simpleToolbarVisibility; OptionsPanelPosition get optionsPanelPosition; RenderResolution get renderResolution; bool get moveOnGesture; List get swamps; PackAssetLocation? get selectedPalette; bool get showVerboseLogs; bool get showThumbnails; bool get bringMovedElementsToFront; List get favoriteTools; + ThemeMode get theme; ThemeDensity get density; double? get limitViewportMultiplier; bool get limitViewportPositive; String get localeTag; String get documentPath; double get gestureSensitivity; double get touchSensitivity; double get selectSensitivity; double get scrollSensitivity; bool? get penOnlyInput; bool get showPenOnlyToggle; bool get inputGestures; String get design; BannerVisibility get bannerVisibility;@JsonKey(includeFromJson: false, includeToJson: false) List get history; bool get zoomEnabled; ZoomPosition get zoomPosition; ZoomPosition get propertyPosition; String? get lastVersion;@JsonKey(includeFromJson: false, includeToJson: false) List get connections; String get defaultRemote; bool get nativeTitleBar; bool get startInFullScreen; bool get navigationRail; IgnorePressure get ignorePressure; SyncMode get syncMode; InputConfiguration get inputConfiguration; String get fallbackPack; List get starred; List get favoriteTemplates; String get defaultTemplate; NavigatorPosition get navigatorPosition; ToolbarPosition get toolbarPosition; ToolbarSize get toolbarSize; SortBy get sortBy; SortOrder get sortOrder; double get imageScale; PlatformTheme get platformTheme;@SRGBConverter() List get recentColors; List get flags; bool get spreadPages; bool get highContrast; bool get gridView; bool get hideExtension; bool get autosave; bool get showSaveButton; int get toolbarRows; bool get delayedAutosave; int get autosaveDelaySeconds; bool get hideCursorWhileDrawing; StartupBehavior get onStartup; SimpleToolbarVisibility get simpleToolbarVisibility; OptionsPanelPosition get optionsPanelPosition; RenderResolution get renderResolution; bool get moveOnGesture; List get swamps; PackAssetLocation? get selectedPalette; bool get showVerboseLogs; bool get showThumbnails; bool get bringMovedElementsToFront; List get favoriteTools; /// Create a copy of ButterflySettings /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -551,21 +551,21 @@ $ButterflySettingsCopyWith get copyWith => _$ButterflySetting void debugFillProperties(DiagnosticPropertiesBuilder properties) { properties ..add(DiagnosticsProperty('type', 'ButterflySettings')) - ..add(DiagnosticsProperty('theme', theme))..add(DiagnosticsProperty('density', density))..add(DiagnosticsProperty('limitViewportMultiplier', limitViewportMultiplier))..add(DiagnosticsProperty('limitViewportPositive', limitViewportPositive))..add(DiagnosticsProperty('localeTag', localeTag))..add(DiagnosticsProperty('documentPath', documentPath))..add(DiagnosticsProperty('gestureSensitivity', gestureSensitivity))..add(DiagnosticsProperty('touchSensitivity', touchSensitivity))..add(DiagnosticsProperty('selectSensitivity', selectSensitivity))..add(DiagnosticsProperty('scrollSensitivity', scrollSensitivity))..add(DiagnosticsProperty('penOnlyInput', penOnlyInput))..add(DiagnosticsProperty('showPenOnlyToggle', showPenOnlyToggle))..add(DiagnosticsProperty('inputGestures', inputGestures))..add(DiagnosticsProperty('design', design))..add(DiagnosticsProperty('bannerVisibility', bannerVisibility))..add(DiagnosticsProperty('history', history))..add(DiagnosticsProperty('zoomEnabled', zoomEnabled))..add(DiagnosticsProperty('zoomPosition', zoomPosition))..add(DiagnosticsProperty('propertyPosition', propertyPosition))..add(DiagnosticsProperty('lastVersion', lastVersion))..add(DiagnosticsProperty('connections', connections))..add(DiagnosticsProperty('defaultRemote', defaultRemote))..add(DiagnosticsProperty('nativeTitleBar', nativeTitleBar))..add(DiagnosticsProperty('startInFullScreen', startInFullScreen))..add(DiagnosticsProperty('navigationRail', navigationRail))..add(DiagnosticsProperty('ignorePressure', ignorePressure))..add(DiagnosticsProperty('syncMode', syncMode))..add(DiagnosticsProperty('inputConfiguration', inputConfiguration))..add(DiagnosticsProperty('fallbackPack', fallbackPack))..add(DiagnosticsProperty('starred', starred))..add(DiagnosticsProperty('favoriteTemplates', favoriteTemplates))..add(DiagnosticsProperty('defaultTemplate', defaultTemplate))..add(DiagnosticsProperty('navigatorPosition', navigatorPosition))..add(DiagnosticsProperty('toolbarPosition', toolbarPosition))..add(DiagnosticsProperty('toolbarSize', toolbarSize))..add(DiagnosticsProperty('sortBy', sortBy))..add(DiagnosticsProperty('sortOrder', sortOrder))..add(DiagnosticsProperty('imageScale', imageScale))..add(DiagnosticsProperty('platformTheme', platformTheme))..add(DiagnosticsProperty('recentColors', recentColors))..add(DiagnosticsProperty('flags', flags))..add(DiagnosticsProperty('spreadPages', spreadPages))..add(DiagnosticsProperty('highContrast', highContrast))..add(DiagnosticsProperty('gridView', gridView))..add(DiagnosticsProperty('hideExtension', hideExtension))..add(DiagnosticsProperty('autosave', autosave))..add(DiagnosticsProperty('showSaveButton', showSaveButton))..add(DiagnosticsProperty('toolbarRows', toolbarRows))..add(DiagnosticsProperty('delayedAutosave', delayedAutosave))..add(DiagnosticsProperty('autosaveDelaySeconds', autosaveDelaySeconds))..add(DiagnosticsProperty('hideCursorWhileDrawing', hideCursorWhileDrawing))..add(DiagnosticsProperty('utilities', utilities))..add(DiagnosticsProperty('onStartup', onStartup))..add(DiagnosticsProperty('simpleToolbarVisibility', simpleToolbarVisibility))..add(DiagnosticsProperty('optionsPanelPosition', optionsPanelPosition))..add(DiagnosticsProperty('renderResolution', renderResolution))..add(DiagnosticsProperty('moveOnGesture', moveOnGesture))..add(DiagnosticsProperty('swamps', swamps))..add(DiagnosticsProperty('selectedPalette', selectedPalette))..add(DiagnosticsProperty('showVerboseLogs', showVerboseLogs))..add(DiagnosticsProperty('showThumbnails', showThumbnails))..add(DiagnosticsProperty('bringMovedElementsToFront', bringMovedElementsToFront))..add(DiagnosticsProperty('favoriteTools', favoriteTools)); + ..add(DiagnosticsProperty('theme', theme))..add(DiagnosticsProperty('density', density))..add(DiagnosticsProperty('limitViewportMultiplier', limitViewportMultiplier))..add(DiagnosticsProperty('limitViewportPositive', limitViewportPositive))..add(DiagnosticsProperty('localeTag', localeTag))..add(DiagnosticsProperty('documentPath', documentPath))..add(DiagnosticsProperty('gestureSensitivity', gestureSensitivity))..add(DiagnosticsProperty('touchSensitivity', touchSensitivity))..add(DiagnosticsProperty('selectSensitivity', selectSensitivity))..add(DiagnosticsProperty('scrollSensitivity', scrollSensitivity))..add(DiagnosticsProperty('penOnlyInput', penOnlyInput))..add(DiagnosticsProperty('showPenOnlyToggle', showPenOnlyToggle))..add(DiagnosticsProperty('inputGestures', inputGestures))..add(DiagnosticsProperty('design', design))..add(DiagnosticsProperty('bannerVisibility', bannerVisibility))..add(DiagnosticsProperty('history', history))..add(DiagnosticsProperty('zoomEnabled', zoomEnabled))..add(DiagnosticsProperty('zoomPosition', zoomPosition))..add(DiagnosticsProperty('propertyPosition', propertyPosition))..add(DiagnosticsProperty('lastVersion', lastVersion))..add(DiagnosticsProperty('connections', connections))..add(DiagnosticsProperty('defaultRemote', defaultRemote))..add(DiagnosticsProperty('nativeTitleBar', nativeTitleBar))..add(DiagnosticsProperty('startInFullScreen', startInFullScreen))..add(DiagnosticsProperty('navigationRail', navigationRail))..add(DiagnosticsProperty('ignorePressure', ignorePressure))..add(DiagnosticsProperty('syncMode', syncMode))..add(DiagnosticsProperty('inputConfiguration', inputConfiguration))..add(DiagnosticsProperty('fallbackPack', fallbackPack))..add(DiagnosticsProperty('starred', starred))..add(DiagnosticsProperty('favoriteTemplates', favoriteTemplates))..add(DiagnosticsProperty('defaultTemplate', defaultTemplate))..add(DiagnosticsProperty('navigatorPosition', navigatorPosition))..add(DiagnosticsProperty('toolbarPosition', toolbarPosition))..add(DiagnosticsProperty('toolbarSize', toolbarSize))..add(DiagnosticsProperty('sortBy', sortBy))..add(DiagnosticsProperty('sortOrder', sortOrder))..add(DiagnosticsProperty('imageScale', imageScale))..add(DiagnosticsProperty('platformTheme', platformTheme))..add(DiagnosticsProperty('recentColors', recentColors))..add(DiagnosticsProperty('flags', flags))..add(DiagnosticsProperty('spreadPages', spreadPages))..add(DiagnosticsProperty('highContrast', highContrast))..add(DiagnosticsProperty('gridView', gridView))..add(DiagnosticsProperty('hideExtension', hideExtension))..add(DiagnosticsProperty('autosave', autosave))..add(DiagnosticsProperty('showSaveButton', showSaveButton))..add(DiagnosticsProperty('toolbarRows', toolbarRows))..add(DiagnosticsProperty('delayedAutosave', delayedAutosave))..add(DiagnosticsProperty('autosaveDelaySeconds', autosaveDelaySeconds))..add(DiagnosticsProperty('hideCursorWhileDrawing', hideCursorWhileDrawing))..add(DiagnosticsProperty('onStartup', onStartup))..add(DiagnosticsProperty('simpleToolbarVisibility', simpleToolbarVisibility))..add(DiagnosticsProperty('optionsPanelPosition', optionsPanelPosition))..add(DiagnosticsProperty('renderResolution', renderResolution))..add(DiagnosticsProperty('moveOnGesture', moveOnGesture))..add(DiagnosticsProperty('swamps', swamps))..add(DiagnosticsProperty('selectedPalette', selectedPalette))..add(DiagnosticsProperty('showVerboseLogs', showVerboseLogs))..add(DiagnosticsProperty('showThumbnails', showThumbnails))..add(DiagnosticsProperty('bringMovedElementsToFront', bringMovedElementsToFront))..add(DiagnosticsProperty('favoriteTools', favoriteTools)); } @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ButterflySettings&&(identical(other.theme, theme) || other.theme == theme)&&(identical(other.density, density) || other.density == density)&&(identical(other.limitViewportMultiplier, limitViewportMultiplier) || other.limitViewportMultiplier == limitViewportMultiplier)&&(identical(other.limitViewportPositive, limitViewportPositive) || other.limitViewportPositive == limitViewportPositive)&&(identical(other.localeTag, localeTag) || other.localeTag == localeTag)&&(identical(other.documentPath, documentPath) || other.documentPath == documentPath)&&(identical(other.gestureSensitivity, gestureSensitivity) || other.gestureSensitivity == gestureSensitivity)&&(identical(other.touchSensitivity, touchSensitivity) || other.touchSensitivity == touchSensitivity)&&(identical(other.selectSensitivity, selectSensitivity) || other.selectSensitivity == selectSensitivity)&&(identical(other.scrollSensitivity, scrollSensitivity) || other.scrollSensitivity == scrollSensitivity)&&(identical(other.penOnlyInput, penOnlyInput) || other.penOnlyInput == penOnlyInput)&&(identical(other.showPenOnlyToggle, showPenOnlyToggle) || other.showPenOnlyToggle == showPenOnlyToggle)&&(identical(other.inputGestures, inputGestures) || other.inputGestures == inputGestures)&&(identical(other.design, design) || other.design == design)&&(identical(other.bannerVisibility, bannerVisibility) || other.bannerVisibility == bannerVisibility)&&const DeepCollectionEquality().equals(other.history, history)&&(identical(other.zoomEnabled, zoomEnabled) || other.zoomEnabled == zoomEnabled)&&(identical(other.zoomPosition, zoomPosition) || other.zoomPosition == zoomPosition)&&(identical(other.propertyPosition, propertyPosition) || other.propertyPosition == propertyPosition)&&(identical(other.lastVersion, lastVersion) || other.lastVersion == lastVersion)&&const DeepCollectionEquality().equals(other.connections, connections)&&(identical(other.defaultRemote, defaultRemote) || other.defaultRemote == defaultRemote)&&(identical(other.nativeTitleBar, nativeTitleBar) || other.nativeTitleBar == nativeTitleBar)&&(identical(other.startInFullScreen, startInFullScreen) || other.startInFullScreen == startInFullScreen)&&(identical(other.navigationRail, navigationRail) || other.navigationRail == navigationRail)&&(identical(other.ignorePressure, ignorePressure) || other.ignorePressure == ignorePressure)&&(identical(other.syncMode, syncMode) || other.syncMode == syncMode)&&(identical(other.inputConfiguration, inputConfiguration) || other.inputConfiguration == inputConfiguration)&&(identical(other.fallbackPack, fallbackPack) || other.fallbackPack == fallbackPack)&&const DeepCollectionEquality().equals(other.starred, starred)&&const DeepCollectionEquality().equals(other.favoriteTemplates, favoriteTemplates)&&(identical(other.defaultTemplate, defaultTemplate) || other.defaultTemplate == defaultTemplate)&&(identical(other.navigatorPosition, navigatorPosition) || other.navigatorPosition == navigatorPosition)&&(identical(other.toolbarPosition, toolbarPosition) || other.toolbarPosition == toolbarPosition)&&(identical(other.toolbarSize, toolbarSize) || other.toolbarSize == toolbarSize)&&(identical(other.sortBy, sortBy) || other.sortBy == sortBy)&&(identical(other.sortOrder, sortOrder) || other.sortOrder == sortOrder)&&(identical(other.imageScale, imageScale) || other.imageScale == imageScale)&&(identical(other.platformTheme, platformTheme) || other.platformTheme == platformTheme)&&const DeepCollectionEquality().equals(other.recentColors, recentColors)&&const DeepCollectionEquality().equals(other.flags, flags)&&(identical(other.spreadPages, spreadPages) || other.spreadPages == spreadPages)&&(identical(other.highContrast, highContrast) || other.highContrast == highContrast)&&(identical(other.gridView, gridView) || other.gridView == gridView)&&(identical(other.hideExtension, hideExtension) || other.hideExtension == hideExtension)&&(identical(other.autosave, autosave) || other.autosave == autosave)&&(identical(other.showSaveButton, showSaveButton) || other.showSaveButton == showSaveButton)&&(identical(other.toolbarRows, toolbarRows) || other.toolbarRows == toolbarRows)&&(identical(other.delayedAutosave, delayedAutosave) || other.delayedAutosave == delayedAutosave)&&(identical(other.autosaveDelaySeconds, autosaveDelaySeconds) || other.autosaveDelaySeconds == autosaveDelaySeconds)&&(identical(other.hideCursorWhileDrawing, hideCursorWhileDrawing) || other.hideCursorWhileDrawing == hideCursorWhileDrawing)&&(identical(other.utilities, utilities) || other.utilities == utilities)&&(identical(other.onStartup, onStartup) || other.onStartup == onStartup)&&(identical(other.simpleToolbarVisibility, simpleToolbarVisibility) || other.simpleToolbarVisibility == simpleToolbarVisibility)&&(identical(other.optionsPanelPosition, optionsPanelPosition) || other.optionsPanelPosition == optionsPanelPosition)&&(identical(other.renderResolution, renderResolution) || other.renderResolution == renderResolution)&&(identical(other.moveOnGesture, moveOnGesture) || other.moveOnGesture == moveOnGesture)&&const DeepCollectionEquality().equals(other.swamps, swamps)&&(identical(other.selectedPalette, selectedPalette) || other.selectedPalette == selectedPalette)&&(identical(other.showVerboseLogs, showVerboseLogs) || other.showVerboseLogs == showVerboseLogs)&&(identical(other.showThumbnails, showThumbnails) || other.showThumbnails == showThumbnails)&&(identical(other.bringMovedElementsToFront, bringMovedElementsToFront) || other.bringMovedElementsToFront == bringMovedElementsToFront)&&const DeepCollectionEquality().equals(other.favoriteTools, favoriteTools)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is ButterflySettings&&(identical(other.theme, theme) || other.theme == theme)&&(identical(other.density, density) || other.density == density)&&(identical(other.limitViewportMultiplier, limitViewportMultiplier) || other.limitViewportMultiplier == limitViewportMultiplier)&&(identical(other.limitViewportPositive, limitViewportPositive) || other.limitViewportPositive == limitViewportPositive)&&(identical(other.localeTag, localeTag) || other.localeTag == localeTag)&&(identical(other.documentPath, documentPath) || other.documentPath == documentPath)&&(identical(other.gestureSensitivity, gestureSensitivity) || other.gestureSensitivity == gestureSensitivity)&&(identical(other.touchSensitivity, touchSensitivity) || other.touchSensitivity == touchSensitivity)&&(identical(other.selectSensitivity, selectSensitivity) || other.selectSensitivity == selectSensitivity)&&(identical(other.scrollSensitivity, scrollSensitivity) || other.scrollSensitivity == scrollSensitivity)&&(identical(other.penOnlyInput, penOnlyInput) || other.penOnlyInput == penOnlyInput)&&(identical(other.showPenOnlyToggle, showPenOnlyToggle) || other.showPenOnlyToggle == showPenOnlyToggle)&&(identical(other.inputGestures, inputGestures) || other.inputGestures == inputGestures)&&(identical(other.design, design) || other.design == design)&&(identical(other.bannerVisibility, bannerVisibility) || other.bannerVisibility == bannerVisibility)&&const DeepCollectionEquality().equals(other.history, history)&&(identical(other.zoomEnabled, zoomEnabled) || other.zoomEnabled == zoomEnabled)&&(identical(other.zoomPosition, zoomPosition) || other.zoomPosition == zoomPosition)&&(identical(other.propertyPosition, propertyPosition) || other.propertyPosition == propertyPosition)&&(identical(other.lastVersion, lastVersion) || other.lastVersion == lastVersion)&&const DeepCollectionEquality().equals(other.connections, connections)&&(identical(other.defaultRemote, defaultRemote) || other.defaultRemote == defaultRemote)&&(identical(other.nativeTitleBar, nativeTitleBar) || other.nativeTitleBar == nativeTitleBar)&&(identical(other.startInFullScreen, startInFullScreen) || other.startInFullScreen == startInFullScreen)&&(identical(other.navigationRail, navigationRail) || other.navigationRail == navigationRail)&&(identical(other.ignorePressure, ignorePressure) || other.ignorePressure == ignorePressure)&&(identical(other.syncMode, syncMode) || other.syncMode == syncMode)&&(identical(other.inputConfiguration, inputConfiguration) || other.inputConfiguration == inputConfiguration)&&(identical(other.fallbackPack, fallbackPack) || other.fallbackPack == fallbackPack)&&const DeepCollectionEquality().equals(other.starred, starred)&&const DeepCollectionEquality().equals(other.favoriteTemplates, favoriteTemplates)&&(identical(other.defaultTemplate, defaultTemplate) || other.defaultTemplate == defaultTemplate)&&(identical(other.navigatorPosition, navigatorPosition) || other.navigatorPosition == navigatorPosition)&&(identical(other.toolbarPosition, toolbarPosition) || other.toolbarPosition == toolbarPosition)&&(identical(other.toolbarSize, toolbarSize) || other.toolbarSize == toolbarSize)&&(identical(other.sortBy, sortBy) || other.sortBy == sortBy)&&(identical(other.sortOrder, sortOrder) || other.sortOrder == sortOrder)&&(identical(other.imageScale, imageScale) || other.imageScale == imageScale)&&(identical(other.platformTheme, platformTheme) || other.platformTheme == platformTheme)&&const DeepCollectionEquality().equals(other.recentColors, recentColors)&&const DeepCollectionEquality().equals(other.flags, flags)&&(identical(other.spreadPages, spreadPages) || other.spreadPages == spreadPages)&&(identical(other.highContrast, highContrast) || other.highContrast == highContrast)&&(identical(other.gridView, gridView) || other.gridView == gridView)&&(identical(other.hideExtension, hideExtension) || other.hideExtension == hideExtension)&&(identical(other.autosave, autosave) || other.autosave == autosave)&&(identical(other.showSaveButton, showSaveButton) || other.showSaveButton == showSaveButton)&&(identical(other.toolbarRows, toolbarRows) || other.toolbarRows == toolbarRows)&&(identical(other.delayedAutosave, delayedAutosave) || other.delayedAutosave == delayedAutosave)&&(identical(other.autosaveDelaySeconds, autosaveDelaySeconds) || other.autosaveDelaySeconds == autosaveDelaySeconds)&&(identical(other.hideCursorWhileDrawing, hideCursorWhileDrawing) || other.hideCursorWhileDrawing == hideCursorWhileDrawing)&&(identical(other.onStartup, onStartup) || other.onStartup == onStartup)&&(identical(other.simpleToolbarVisibility, simpleToolbarVisibility) || other.simpleToolbarVisibility == simpleToolbarVisibility)&&(identical(other.optionsPanelPosition, optionsPanelPosition) || other.optionsPanelPosition == optionsPanelPosition)&&(identical(other.renderResolution, renderResolution) || other.renderResolution == renderResolution)&&(identical(other.moveOnGesture, moveOnGesture) || other.moveOnGesture == moveOnGesture)&&const DeepCollectionEquality().equals(other.swamps, swamps)&&(identical(other.selectedPalette, selectedPalette) || other.selectedPalette == selectedPalette)&&(identical(other.showVerboseLogs, showVerboseLogs) || other.showVerboseLogs == showVerboseLogs)&&(identical(other.showThumbnails, showThumbnails) || other.showThumbnails == showThumbnails)&&(identical(other.bringMovedElementsToFront, bringMovedElementsToFront) || other.bringMovedElementsToFront == bringMovedElementsToFront)&&const DeepCollectionEquality().equals(other.favoriteTools, favoriteTools)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hashAll([runtimeType,theme,density,limitViewportMultiplier,limitViewportPositive,localeTag,documentPath,gestureSensitivity,touchSensitivity,selectSensitivity,scrollSensitivity,penOnlyInput,showPenOnlyToggle,inputGestures,design,bannerVisibility,const DeepCollectionEquality().hash(history),zoomEnabled,zoomPosition,propertyPosition,lastVersion,const DeepCollectionEquality().hash(connections),defaultRemote,nativeTitleBar,startInFullScreen,navigationRail,ignorePressure,syncMode,inputConfiguration,fallbackPack,const DeepCollectionEquality().hash(starred),const DeepCollectionEquality().hash(favoriteTemplates),defaultTemplate,navigatorPosition,toolbarPosition,toolbarSize,sortBy,sortOrder,imageScale,platformTheme,const DeepCollectionEquality().hash(recentColors),const DeepCollectionEquality().hash(flags),spreadPages,highContrast,gridView,hideExtension,autosave,showSaveButton,toolbarRows,delayedAutosave,autosaveDelaySeconds,hideCursorWhileDrawing,utilities,onStartup,simpleToolbarVisibility,optionsPanelPosition,renderResolution,moveOnGesture,const DeepCollectionEquality().hash(swamps),selectedPalette,showVerboseLogs,showThumbnails,bringMovedElementsToFront,const DeepCollectionEquality().hash(favoriteTools)]); +int get hashCode => Object.hashAll([runtimeType,theme,density,limitViewportMultiplier,limitViewportPositive,localeTag,documentPath,gestureSensitivity,touchSensitivity,selectSensitivity,scrollSensitivity,penOnlyInput,showPenOnlyToggle,inputGestures,design,bannerVisibility,const DeepCollectionEquality().hash(history),zoomEnabled,zoomPosition,propertyPosition,lastVersion,const DeepCollectionEquality().hash(connections),defaultRemote,nativeTitleBar,startInFullScreen,navigationRail,ignorePressure,syncMode,inputConfiguration,fallbackPack,const DeepCollectionEquality().hash(starred),const DeepCollectionEquality().hash(favoriteTemplates),defaultTemplate,navigatorPosition,toolbarPosition,toolbarSize,sortBy,sortOrder,imageScale,platformTheme,const DeepCollectionEquality().hash(recentColors),const DeepCollectionEquality().hash(flags),spreadPages,highContrast,gridView,hideExtension,autosave,showSaveButton,toolbarRows,delayedAutosave,autosaveDelaySeconds,hideCursorWhileDrawing,onStartup,simpleToolbarVisibility,optionsPanelPosition,renderResolution,moveOnGesture,const DeepCollectionEquality().hash(swamps),selectedPalette,showVerboseLogs,showThumbnails,bringMovedElementsToFront,const DeepCollectionEquality().hash(favoriteTools)]); @override String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'ButterflySettings(theme: $theme, density: $density, limitViewportMultiplier: $limitViewportMultiplier, limitViewportPositive: $limitViewportPositive, localeTag: $localeTag, documentPath: $documentPath, gestureSensitivity: $gestureSensitivity, touchSensitivity: $touchSensitivity, selectSensitivity: $selectSensitivity, scrollSensitivity: $scrollSensitivity, penOnlyInput: $penOnlyInput, showPenOnlyToggle: $showPenOnlyToggle, inputGestures: $inputGestures, design: $design, bannerVisibility: $bannerVisibility, history: $history, zoomEnabled: $zoomEnabled, zoomPosition: $zoomPosition, propertyPosition: $propertyPosition, lastVersion: $lastVersion, connections: $connections, defaultRemote: $defaultRemote, nativeTitleBar: $nativeTitleBar, startInFullScreen: $startInFullScreen, navigationRail: $navigationRail, ignorePressure: $ignorePressure, syncMode: $syncMode, inputConfiguration: $inputConfiguration, fallbackPack: $fallbackPack, starred: $starred, favoriteTemplates: $favoriteTemplates, defaultTemplate: $defaultTemplate, navigatorPosition: $navigatorPosition, toolbarPosition: $toolbarPosition, toolbarSize: $toolbarSize, sortBy: $sortBy, sortOrder: $sortOrder, imageScale: $imageScale, platformTheme: $platformTheme, recentColors: $recentColors, flags: $flags, spreadPages: $spreadPages, highContrast: $highContrast, gridView: $gridView, hideExtension: $hideExtension, autosave: $autosave, showSaveButton: $showSaveButton, toolbarRows: $toolbarRows, delayedAutosave: $delayedAutosave, autosaveDelaySeconds: $autosaveDelaySeconds, hideCursorWhileDrawing: $hideCursorWhileDrawing, utilities: $utilities, onStartup: $onStartup, simpleToolbarVisibility: $simpleToolbarVisibility, optionsPanelPosition: $optionsPanelPosition, renderResolution: $renderResolution, moveOnGesture: $moveOnGesture, swamps: $swamps, selectedPalette: $selectedPalette, showVerboseLogs: $showVerboseLogs, showThumbnails: $showThumbnails, bringMovedElementsToFront: $bringMovedElementsToFront, favoriteTools: $favoriteTools)'; + return 'ButterflySettings(theme: $theme, density: $density, limitViewportMultiplier: $limitViewportMultiplier, limitViewportPositive: $limitViewportPositive, localeTag: $localeTag, documentPath: $documentPath, gestureSensitivity: $gestureSensitivity, touchSensitivity: $touchSensitivity, selectSensitivity: $selectSensitivity, scrollSensitivity: $scrollSensitivity, penOnlyInput: $penOnlyInput, showPenOnlyToggle: $showPenOnlyToggle, inputGestures: $inputGestures, design: $design, bannerVisibility: $bannerVisibility, history: $history, zoomEnabled: $zoomEnabled, zoomPosition: $zoomPosition, propertyPosition: $propertyPosition, lastVersion: $lastVersion, connections: $connections, defaultRemote: $defaultRemote, nativeTitleBar: $nativeTitleBar, startInFullScreen: $startInFullScreen, navigationRail: $navigationRail, ignorePressure: $ignorePressure, syncMode: $syncMode, inputConfiguration: $inputConfiguration, fallbackPack: $fallbackPack, starred: $starred, favoriteTemplates: $favoriteTemplates, defaultTemplate: $defaultTemplate, navigatorPosition: $navigatorPosition, toolbarPosition: $toolbarPosition, toolbarSize: $toolbarSize, sortBy: $sortBy, sortOrder: $sortOrder, imageScale: $imageScale, platformTheme: $platformTheme, recentColors: $recentColors, flags: $flags, spreadPages: $spreadPages, highContrast: $highContrast, gridView: $gridView, hideExtension: $hideExtension, autosave: $autosave, showSaveButton: $showSaveButton, toolbarRows: $toolbarRows, delayedAutosave: $delayedAutosave, autosaveDelaySeconds: $autosaveDelaySeconds, hideCursorWhileDrawing: $hideCursorWhileDrawing, onStartup: $onStartup, simpleToolbarVisibility: $simpleToolbarVisibility, optionsPanelPosition: $optionsPanelPosition, renderResolution: $renderResolution, moveOnGesture: $moveOnGesture, swamps: $swamps, selectedPalette: $selectedPalette, showVerboseLogs: $showVerboseLogs, showThumbnails: $showThumbnails, bringMovedElementsToFront: $bringMovedElementsToFront, favoriteTools: $favoriteTools)'; } @@ -576,11 +576,11 @@ abstract mixin class $ButterflySettingsCopyWith<$Res> { factory $ButterflySettingsCopyWith(ButterflySettings value, $Res Function(ButterflySettings) _then) = _$ButterflySettingsCopyWithImpl; @useResult $Res call({ - ThemeMode theme, ThemeDensity density, double? limitViewportMultiplier, bool limitViewportPositive, String localeTag, String documentPath, double gestureSensitivity, double touchSensitivity, double selectSensitivity, double scrollSensitivity, bool? penOnlyInput, bool showPenOnlyToggle, bool inputGestures, String design, BannerVisibility bannerVisibility,@JsonKey(includeFromJson: false, includeToJson: false) List history, bool zoomEnabled, ZoomPosition zoomPosition, ZoomPosition propertyPosition, String? lastVersion,@JsonKey(includeFromJson: false, includeToJson: false) List connections, String defaultRemote, bool nativeTitleBar, bool startInFullScreen, bool navigationRail, IgnorePressure ignorePressure, SyncMode syncMode, InputConfiguration inputConfiguration, String fallbackPack, List starred, List favoriteTemplates, String defaultTemplate, NavigatorPosition navigatorPosition, ToolbarPosition toolbarPosition, ToolbarSize toolbarSize, SortBy sortBy, SortOrder sortOrder, double imageScale, PlatformTheme platformTheme,@SRGBConverter() List recentColors, List flags, bool spreadPages, bool highContrast, bool gridView, bool hideExtension, bool autosave, bool showSaveButton, int toolbarRows, bool delayedAutosave, int autosaveDelaySeconds, bool hideCursorWhileDrawing, UtilitiesState utilities, StartupBehavior onStartup, SimpleToolbarVisibility simpleToolbarVisibility, OptionsPanelPosition optionsPanelPosition, RenderResolution renderResolution, bool moveOnGesture, List swamps, PackAssetLocation? selectedPalette, bool showVerboseLogs, bool showThumbnails, bool bringMovedElementsToFront, List favoriteTools + ThemeMode theme, ThemeDensity density, double? limitViewportMultiplier, bool limitViewportPositive, String localeTag, String documentPath, double gestureSensitivity, double touchSensitivity, double selectSensitivity, double scrollSensitivity, bool? penOnlyInput, bool showPenOnlyToggle, bool inputGestures, String design, BannerVisibility bannerVisibility,@JsonKey(includeFromJson: false, includeToJson: false) List history, bool zoomEnabled, ZoomPosition zoomPosition, ZoomPosition propertyPosition, String? lastVersion,@JsonKey(includeFromJson: false, includeToJson: false) List connections, String defaultRemote, bool nativeTitleBar, bool startInFullScreen, bool navigationRail, IgnorePressure ignorePressure, SyncMode syncMode, InputConfiguration inputConfiguration, String fallbackPack, List starred, List favoriteTemplates, String defaultTemplate, NavigatorPosition navigatorPosition, ToolbarPosition toolbarPosition, ToolbarSize toolbarSize, SortBy sortBy, SortOrder sortOrder, double imageScale, PlatformTheme platformTheme,@SRGBConverter() List recentColors, List flags, bool spreadPages, bool highContrast, bool gridView, bool hideExtension, bool autosave, bool showSaveButton, int toolbarRows, bool delayedAutosave, int autosaveDelaySeconds, bool hideCursorWhileDrawing, StartupBehavior onStartup, SimpleToolbarVisibility simpleToolbarVisibility, OptionsPanelPosition optionsPanelPosition, RenderResolution renderResolution, bool moveOnGesture, List swamps, PackAssetLocation? selectedPalette, bool showVerboseLogs, bool showThumbnails, bool bringMovedElementsToFront, List favoriteTools }); -$InputConfigurationCopyWith<$Res> get inputConfiguration;$UtilitiesStateCopyWith<$Res> get utilities;$PackAssetLocationCopyWith<$Res>? get selectedPalette; +$InputConfigurationCopyWith<$Res> get inputConfiguration;$PackAssetLocationCopyWith<$Res>? get selectedPalette; } /// @nodoc @@ -593,7 +593,7 @@ class _$ButterflySettingsCopyWithImpl<$Res> /// Create a copy of ButterflySettings /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? theme = null,Object? density = null,Object? limitViewportMultiplier = freezed,Object? limitViewportPositive = null,Object? localeTag = null,Object? documentPath = null,Object? gestureSensitivity = null,Object? touchSensitivity = null,Object? selectSensitivity = null,Object? scrollSensitivity = null,Object? penOnlyInput = freezed,Object? showPenOnlyToggle = null,Object? inputGestures = null,Object? design = null,Object? bannerVisibility = null,Object? history = null,Object? zoomEnabled = null,Object? zoomPosition = null,Object? propertyPosition = null,Object? lastVersion = freezed,Object? connections = null,Object? defaultRemote = null,Object? nativeTitleBar = null,Object? startInFullScreen = null,Object? navigationRail = null,Object? ignorePressure = null,Object? syncMode = null,Object? inputConfiguration = null,Object? fallbackPack = null,Object? starred = null,Object? favoriteTemplates = null,Object? defaultTemplate = null,Object? navigatorPosition = null,Object? toolbarPosition = null,Object? toolbarSize = null,Object? sortBy = null,Object? sortOrder = null,Object? imageScale = null,Object? platformTheme = null,Object? recentColors = null,Object? flags = null,Object? spreadPages = null,Object? highContrast = null,Object? gridView = null,Object? hideExtension = null,Object? autosave = null,Object? showSaveButton = null,Object? toolbarRows = null,Object? delayedAutosave = null,Object? autosaveDelaySeconds = null,Object? hideCursorWhileDrawing = null,Object? utilities = null,Object? onStartup = null,Object? simpleToolbarVisibility = null,Object? optionsPanelPosition = null,Object? renderResolution = null,Object? moveOnGesture = null,Object? swamps = null,Object? selectedPalette = freezed,Object? showVerboseLogs = null,Object? showThumbnails = null,Object? bringMovedElementsToFront = null,Object? favoriteTools = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? theme = null,Object? density = null,Object? limitViewportMultiplier = freezed,Object? limitViewportPositive = null,Object? localeTag = null,Object? documentPath = null,Object? gestureSensitivity = null,Object? touchSensitivity = null,Object? selectSensitivity = null,Object? scrollSensitivity = null,Object? penOnlyInput = freezed,Object? showPenOnlyToggle = null,Object? inputGestures = null,Object? design = null,Object? bannerVisibility = null,Object? history = null,Object? zoomEnabled = null,Object? zoomPosition = null,Object? propertyPosition = null,Object? lastVersion = freezed,Object? connections = null,Object? defaultRemote = null,Object? nativeTitleBar = null,Object? startInFullScreen = null,Object? navigationRail = null,Object? ignorePressure = null,Object? syncMode = null,Object? inputConfiguration = null,Object? fallbackPack = null,Object? starred = null,Object? favoriteTemplates = null,Object? defaultTemplate = null,Object? navigatorPosition = null,Object? toolbarPosition = null,Object? toolbarSize = null,Object? sortBy = null,Object? sortOrder = null,Object? imageScale = null,Object? platformTheme = null,Object? recentColors = null,Object? flags = null,Object? spreadPages = null,Object? highContrast = null,Object? gridView = null,Object? hideExtension = null,Object? autosave = null,Object? showSaveButton = null,Object? toolbarRows = null,Object? delayedAutosave = null,Object? autosaveDelaySeconds = null,Object? hideCursorWhileDrawing = null,Object? onStartup = null,Object? simpleToolbarVisibility = null,Object? optionsPanelPosition = null,Object? renderResolution = null,Object? moveOnGesture = null,Object? swamps = null,Object? selectedPalette = freezed,Object? showVerboseLogs = null,Object? showThumbnails = null,Object? bringMovedElementsToFront = null,Object? favoriteTools = null,}) { return _then(_self.copyWith( theme: null == theme ? _self.theme : theme // ignore: cast_nullable_to_non_nullable as ThemeMode,density: null == density ? _self.density : density // ignore: cast_nullable_to_non_nullable @@ -646,8 +646,7 @@ as bool,toolbarRows: null == toolbarRows ? _self.toolbarRows : toolbarRows // ig as int,delayedAutosave: null == delayedAutosave ? _self.delayedAutosave : delayedAutosave // ignore: cast_nullable_to_non_nullable as bool,autosaveDelaySeconds: null == autosaveDelaySeconds ? _self.autosaveDelaySeconds : autosaveDelaySeconds // ignore: cast_nullable_to_non_nullable as int,hideCursorWhileDrawing: null == hideCursorWhileDrawing ? _self.hideCursorWhileDrawing : hideCursorWhileDrawing // ignore: cast_nullable_to_non_nullable -as bool,utilities: null == utilities ? _self.utilities : utilities // ignore: cast_nullable_to_non_nullable -as UtilitiesState,onStartup: null == onStartup ? _self.onStartup : onStartup // ignore: cast_nullable_to_non_nullable +as bool,onStartup: null == onStartup ? _self.onStartup : onStartup // ignore: cast_nullable_to_non_nullable as StartupBehavior,simpleToolbarVisibility: null == simpleToolbarVisibility ? _self.simpleToolbarVisibility : simpleToolbarVisibility // ignore: cast_nullable_to_non_nullable as SimpleToolbarVisibility,optionsPanelPosition: null == optionsPanelPosition ? _self.optionsPanelPosition : optionsPanelPosition // ignore: cast_nullable_to_non_nullable as OptionsPanelPosition,renderResolution: null == renderResolution ? _self.renderResolution : renderResolution // ignore: cast_nullable_to_non_nullable @@ -674,15 +673,6 @@ $InputConfigurationCopyWith<$Res> get inputConfiguration { /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') -$UtilitiesStateCopyWith<$Res> get utilities { - - return $UtilitiesStateCopyWith<$Res>(_self.utilities, (value) { - return _then(_self.copyWith(utilities: value)); - }); -}/// Create a copy of ButterflySettings -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') $PackAssetLocationCopyWith<$Res>? get selectedPalette { if (_self.selectedPalette == null) { return null; @@ -700,7 +690,7 @@ $PackAssetLocationCopyWith<$Res>? get selectedPalette { @JsonSerializable() class _ButterflySettings extends ButterflySettings with DiagnosticableTreeMixin { - const _ButterflySettings({this.theme = ThemeMode.system, this.density = ThemeDensity.system, this.limitViewportMultiplier, this.limitViewportPositive = false, this.localeTag = '', this.documentPath = '', this.gestureSensitivity = 1, this.touchSensitivity = 1, this.selectSensitivity = 1, this.scrollSensitivity = 1, this.penOnlyInput, this.showPenOnlyToggle = true, this.inputGestures = true, this.design = '', this.bannerVisibility = BannerVisibility.always, @JsonKey(includeFromJson: false, includeToJson: false) final List history = const [], this.zoomEnabled = true, this.zoomPosition = ZoomPosition.bottomRight, this.propertyPosition = ZoomPosition.topRight, this.lastVersion, @JsonKey(includeFromJson: false, includeToJson: false) final List connections = const [], this.defaultRemote = '', this.nativeTitleBar = false, this.startInFullScreen = false, this.navigationRail = true, this.ignorePressure = IgnorePressure.first, this.syncMode = SyncMode.noMobile, this.inputConfiguration = const InputConfiguration(), this.fallbackPack = '', final List starred = const [], final List favoriteTemplates = const [], this.defaultTemplate = '', this.navigatorPosition = NavigatorPosition.left, this.toolbarPosition = ToolbarPosition.inline, this.toolbarSize = ToolbarSize.normal, this.sortBy = SortBy.modified, this.sortOrder = SortOrder.descending, this.imageScale = 0.5, this.platformTheme = PlatformTheme.system, @SRGBConverter() final List recentColors = const [], final List flags = const [], this.spreadPages = false, this.highContrast = false, this.gridView = false, this.hideExtension = true, this.autosave = true, this.showSaveButton = true, this.toolbarRows = 1, this.delayedAutosave = true, this.autosaveDelaySeconds = 3, this.hideCursorWhileDrawing = false, this.utilities = const UtilitiesState(), this.onStartup = StartupBehavior.openHomeScreen, this.simpleToolbarVisibility = SimpleToolbarVisibility.show, this.optionsPanelPosition = OptionsPanelPosition.top, this.renderResolution = RenderResolution.normal, this.moveOnGesture = true, final List swamps = const [], this.selectedPalette, this.showVerboseLogs = false, this.showThumbnails = true, this.bringMovedElementsToFront = false, final List favoriteTools = const []}): _history = history,_connections = connections,_starred = starred,_favoriteTemplates = favoriteTemplates,_recentColors = recentColors,_flags = flags,_swamps = swamps,_favoriteTools = favoriteTools,super._(); + const _ButterflySettings({this.theme = ThemeMode.system, this.density = ThemeDensity.system, this.limitViewportMultiplier, this.limitViewportPositive = false, this.localeTag = '', this.documentPath = '', this.gestureSensitivity = 1, this.touchSensitivity = 1, this.selectSensitivity = 1, this.scrollSensitivity = 1, this.penOnlyInput, this.showPenOnlyToggle = true, this.inputGestures = true, this.design = '', this.bannerVisibility = BannerVisibility.always, @JsonKey(includeFromJson: false, includeToJson: false) final List history = const [], this.zoomEnabled = true, this.zoomPosition = ZoomPosition.bottomRight, this.propertyPosition = ZoomPosition.topRight, this.lastVersion, @JsonKey(includeFromJson: false, includeToJson: false) final List connections = const [], this.defaultRemote = '', this.nativeTitleBar = false, this.startInFullScreen = false, this.navigationRail = true, this.ignorePressure = IgnorePressure.first, this.syncMode = SyncMode.noMobile, this.inputConfiguration = const InputConfiguration(), this.fallbackPack = '', final List starred = const [], final List favoriteTemplates = const [], this.defaultTemplate = '', this.navigatorPosition = NavigatorPosition.left, this.toolbarPosition = ToolbarPosition.inline, this.toolbarSize = ToolbarSize.normal, this.sortBy = SortBy.modified, this.sortOrder = SortOrder.descending, this.imageScale = 0.5, this.platformTheme = PlatformTheme.system, @SRGBConverter() final List recentColors = const [], final List flags = const [], this.spreadPages = false, this.highContrast = false, this.gridView = false, this.hideExtension = true, this.autosave = true, this.showSaveButton = true, this.toolbarRows = 1, this.delayedAutosave = true, this.autosaveDelaySeconds = 3, this.hideCursorWhileDrawing = false, this.onStartup = StartupBehavior.openHomeScreen, this.simpleToolbarVisibility = SimpleToolbarVisibility.show, this.optionsPanelPosition = OptionsPanelPosition.top, this.renderResolution = RenderResolution.normal, this.moveOnGesture = true, final List swamps = const [], this.selectedPalette, this.showVerboseLogs = false, this.showThumbnails = true, this.bringMovedElementsToFront = false, final List favoriteTools = const []}): _history = history,_connections = connections,_starred = starred,_favoriteTemplates = favoriteTemplates,_recentColors = recentColors,_flags = flags,_swamps = swamps,_favoriteTools = favoriteTools,super._(); factory _ButterflySettings.fromJson(Map json) => _$ButterflySettingsFromJson(json); @override@JsonKey() final ThemeMode theme; @@ -790,7 +780,6 @@ class _ButterflySettings extends ButterflySettings with DiagnosticableTreeMixin @override@JsonKey() final bool delayedAutosave; @override@JsonKey() final int autosaveDelaySeconds; @override@JsonKey() final bool hideCursorWhileDrawing; -@override@JsonKey() final UtilitiesState utilities; @override@JsonKey() final StartupBehavior onStartup; @override@JsonKey() final SimpleToolbarVisibility simpleToolbarVisibility; @override@JsonKey() final OptionsPanelPosition optionsPanelPosition; @@ -829,21 +818,21 @@ Map toJson() { void debugFillProperties(DiagnosticPropertiesBuilder properties) { properties ..add(DiagnosticsProperty('type', 'ButterflySettings')) - ..add(DiagnosticsProperty('theme', theme))..add(DiagnosticsProperty('density', density))..add(DiagnosticsProperty('limitViewportMultiplier', limitViewportMultiplier))..add(DiagnosticsProperty('limitViewportPositive', limitViewportPositive))..add(DiagnosticsProperty('localeTag', localeTag))..add(DiagnosticsProperty('documentPath', documentPath))..add(DiagnosticsProperty('gestureSensitivity', gestureSensitivity))..add(DiagnosticsProperty('touchSensitivity', touchSensitivity))..add(DiagnosticsProperty('selectSensitivity', selectSensitivity))..add(DiagnosticsProperty('scrollSensitivity', scrollSensitivity))..add(DiagnosticsProperty('penOnlyInput', penOnlyInput))..add(DiagnosticsProperty('showPenOnlyToggle', showPenOnlyToggle))..add(DiagnosticsProperty('inputGestures', inputGestures))..add(DiagnosticsProperty('design', design))..add(DiagnosticsProperty('bannerVisibility', bannerVisibility))..add(DiagnosticsProperty('history', history))..add(DiagnosticsProperty('zoomEnabled', zoomEnabled))..add(DiagnosticsProperty('zoomPosition', zoomPosition))..add(DiagnosticsProperty('propertyPosition', propertyPosition))..add(DiagnosticsProperty('lastVersion', lastVersion))..add(DiagnosticsProperty('connections', connections))..add(DiagnosticsProperty('defaultRemote', defaultRemote))..add(DiagnosticsProperty('nativeTitleBar', nativeTitleBar))..add(DiagnosticsProperty('startInFullScreen', startInFullScreen))..add(DiagnosticsProperty('navigationRail', navigationRail))..add(DiagnosticsProperty('ignorePressure', ignorePressure))..add(DiagnosticsProperty('syncMode', syncMode))..add(DiagnosticsProperty('inputConfiguration', inputConfiguration))..add(DiagnosticsProperty('fallbackPack', fallbackPack))..add(DiagnosticsProperty('starred', starred))..add(DiagnosticsProperty('favoriteTemplates', favoriteTemplates))..add(DiagnosticsProperty('defaultTemplate', defaultTemplate))..add(DiagnosticsProperty('navigatorPosition', navigatorPosition))..add(DiagnosticsProperty('toolbarPosition', toolbarPosition))..add(DiagnosticsProperty('toolbarSize', toolbarSize))..add(DiagnosticsProperty('sortBy', sortBy))..add(DiagnosticsProperty('sortOrder', sortOrder))..add(DiagnosticsProperty('imageScale', imageScale))..add(DiagnosticsProperty('platformTheme', platformTheme))..add(DiagnosticsProperty('recentColors', recentColors))..add(DiagnosticsProperty('flags', flags))..add(DiagnosticsProperty('spreadPages', spreadPages))..add(DiagnosticsProperty('highContrast', highContrast))..add(DiagnosticsProperty('gridView', gridView))..add(DiagnosticsProperty('hideExtension', hideExtension))..add(DiagnosticsProperty('autosave', autosave))..add(DiagnosticsProperty('showSaveButton', showSaveButton))..add(DiagnosticsProperty('toolbarRows', toolbarRows))..add(DiagnosticsProperty('delayedAutosave', delayedAutosave))..add(DiagnosticsProperty('autosaveDelaySeconds', autosaveDelaySeconds))..add(DiagnosticsProperty('hideCursorWhileDrawing', hideCursorWhileDrawing))..add(DiagnosticsProperty('utilities', utilities))..add(DiagnosticsProperty('onStartup', onStartup))..add(DiagnosticsProperty('simpleToolbarVisibility', simpleToolbarVisibility))..add(DiagnosticsProperty('optionsPanelPosition', optionsPanelPosition))..add(DiagnosticsProperty('renderResolution', renderResolution))..add(DiagnosticsProperty('moveOnGesture', moveOnGesture))..add(DiagnosticsProperty('swamps', swamps))..add(DiagnosticsProperty('selectedPalette', selectedPalette))..add(DiagnosticsProperty('showVerboseLogs', showVerboseLogs))..add(DiagnosticsProperty('showThumbnails', showThumbnails))..add(DiagnosticsProperty('bringMovedElementsToFront', bringMovedElementsToFront))..add(DiagnosticsProperty('favoriteTools', favoriteTools)); + ..add(DiagnosticsProperty('theme', theme))..add(DiagnosticsProperty('density', density))..add(DiagnosticsProperty('limitViewportMultiplier', limitViewportMultiplier))..add(DiagnosticsProperty('limitViewportPositive', limitViewportPositive))..add(DiagnosticsProperty('localeTag', localeTag))..add(DiagnosticsProperty('documentPath', documentPath))..add(DiagnosticsProperty('gestureSensitivity', gestureSensitivity))..add(DiagnosticsProperty('touchSensitivity', touchSensitivity))..add(DiagnosticsProperty('selectSensitivity', selectSensitivity))..add(DiagnosticsProperty('scrollSensitivity', scrollSensitivity))..add(DiagnosticsProperty('penOnlyInput', penOnlyInput))..add(DiagnosticsProperty('showPenOnlyToggle', showPenOnlyToggle))..add(DiagnosticsProperty('inputGestures', inputGestures))..add(DiagnosticsProperty('design', design))..add(DiagnosticsProperty('bannerVisibility', bannerVisibility))..add(DiagnosticsProperty('history', history))..add(DiagnosticsProperty('zoomEnabled', zoomEnabled))..add(DiagnosticsProperty('zoomPosition', zoomPosition))..add(DiagnosticsProperty('propertyPosition', propertyPosition))..add(DiagnosticsProperty('lastVersion', lastVersion))..add(DiagnosticsProperty('connections', connections))..add(DiagnosticsProperty('defaultRemote', defaultRemote))..add(DiagnosticsProperty('nativeTitleBar', nativeTitleBar))..add(DiagnosticsProperty('startInFullScreen', startInFullScreen))..add(DiagnosticsProperty('navigationRail', navigationRail))..add(DiagnosticsProperty('ignorePressure', ignorePressure))..add(DiagnosticsProperty('syncMode', syncMode))..add(DiagnosticsProperty('inputConfiguration', inputConfiguration))..add(DiagnosticsProperty('fallbackPack', fallbackPack))..add(DiagnosticsProperty('starred', starred))..add(DiagnosticsProperty('favoriteTemplates', favoriteTemplates))..add(DiagnosticsProperty('defaultTemplate', defaultTemplate))..add(DiagnosticsProperty('navigatorPosition', navigatorPosition))..add(DiagnosticsProperty('toolbarPosition', toolbarPosition))..add(DiagnosticsProperty('toolbarSize', toolbarSize))..add(DiagnosticsProperty('sortBy', sortBy))..add(DiagnosticsProperty('sortOrder', sortOrder))..add(DiagnosticsProperty('imageScale', imageScale))..add(DiagnosticsProperty('platformTheme', platformTheme))..add(DiagnosticsProperty('recentColors', recentColors))..add(DiagnosticsProperty('flags', flags))..add(DiagnosticsProperty('spreadPages', spreadPages))..add(DiagnosticsProperty('highContrast', highContrast))..add(DiagnosticsProperty('gridView', gridView))..add(DiagnosticsProperty('hideExtension', hideExtension))..add(DiagnosticsProperty('autosave', autosave))..add(DiagnosticsProperty('showSaveButton', showSaveButton))..add(DiagnosticsProperty('toolbarRows', toolbarRows))..add(DiagnosticsProperty('delayedAutosave', delayedAutosave))..add(DiagnosticsProperty('autosaveDelaySeconds', autosaveDelaySeconds))..add(DiagnosticsProperty('hideCursorWhileDrawing', hideCursorWhileDrawing))..add(DiagnosticsProperty('onStartup', onStartup))..add(DiagnosticsProperty('simpleToolbarVisibility', simpleToolbarVisibility))..add(DiagnosticsProperty('optionsPanelPosition', optionsPanelPosition))..add(DiagnosticsProperty('renderResolution', renderResolution))..add(DiagnosticsProperty('moveOnGesture', moveOnGesture))..add(DiagnosticsProperty('swamps', swamps))..add(DiagnosticsProperty('selectedPalette', selectedPalette))..add(DiagnosticsProperty('showVerboseLogs', showVerboseLogs))..add(DiagnosticsProperty('showThumbnails', showThumbnails))..add(DiagnosticsProperty('bringMovedElementsToFront', bringMovedElementsToFront))..add(DiagnosticsProperty('favoriteTools', favoriteTools)); } @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _ButterflySettings&&(identical(other.theme, theme) || other.theme == theme)&&(identical(other.density, density) || other.density == density)&&(identical(other.limitViewportMultiplier, limitViewportMultiplier) || other.limitViewportMultiplier == limitViewportMultiplier)&&(identical(other.limitViewportPositive, limitViewportPositive) || other.limitViewportPositive == limitViewportPositive)&&(identical(other.localeTag, localeTag) || other.localeTag == localeTag)&&(identical(other.documentPath, documentPath) || other.documentPath == documentPath)&&(identical(other.gestureSensitivity, gestureSensitivity) || other.gestureSensitivity == gestureSensitivity)&&(identical(other.touchSensitivity, touchSensitivity) || other.touchSensitivity == touchSensitivity)&&(identical(other.selectSensitivity, selectSensitivity) || other.selectSensitivity == selectSensitivity)&&(identical(other.scrollSensitivity, scrollSensitivity) || other.scrollSensitivity == scrollSensitivity)&&(identical(other.penOnlyInput, penOnlyInput) || other.penOnlyInput == penOnlyInput)&&(identical(other.showPenOnlyToggle, showPenOnlyToggle) || other.showPenOnlyToggle == showPenOnlyToggle)&&(identical(other.inputGestures, inputGestures) || other.inputGestures == inputGestures)&&(identical(other.design, design) || other.design == design)&&(identical(other.bannerVisibility, bannerVisibility) || other.bannerVisibility == bannerVisibility)&&const DeepCollectionEquality().equals(other._history, _history)&&(identical(other.zoomEnabled, zoomEnabled) || other.zoomEnabled == zoomEnabled)&&(identical(other.zoomPosition, zoomPosition) || other.zoomPosition == zoomPosition)&&(identical(other.propertyPosition, propertyPosition) || other.propertyPosition == propertyPosition)&&(identical(other.lastVersion, lastVersion) || other.lastVersion == lastVersion)&&const DeepCollectionEquality().equals(other._connections, _connections)&&(identical(other.defaultRemote, defaultRemote) || other.defaultRemote == defaultRemote)&&(identical(other.nativeTitleBar, nativeTitleBar) || other.nativeTitleBar == nativeTitleBar)&&(identical(other.startInFullScreen, startInFullScreen) || other.startInFullScreen == startInFullScreen)&&(identical(other.navigationRail, navigationRail) || other.navigationRail == navigationRail)&&(identical(other.ignorePressure, ignorePressure) || other.ignorePressure == ignorePressure)&&(identical(other.syncMode, syncMode) || other.syncMode == syncMode)&&(identical(other.inputConfiguration, inputConfiguration) || other.inputConfiguration == inputConfiguration)&&(identical(other.fallbackPack, fallbackPack) || other.fallbackPack == fallbackPack)&&const DeepCollectionEquality().equals(other._starred, _starred)&&const DeepCollectionEquality().equals(other._favoriteTemplates, _favoriteTemplates)&&(identical(other.defaultTemplate, defaultTemplate) || other.defaultTemplate == defaultTemplate)&&(identical(other.navigatorPosition, navigatorPosition) || other.navigatorPosition == navigatorPosition)&&(identical(other.toolbarPosition, toolbarPosition) || other.toolbarPosition == toolbarPosition)&&(identical(other.toolbarSize, toolbarSize) || other.toolbarSize == toolbarSize)&&(identical(other.sortBy, sortBy) || other.sortBy == sortBy)&&(identical(other.sortOrder, sortOrder) || other.sortOrder == sortOrder)&&(identical(other.imageScale, imageScale) || other.imageScale == imageScale)&&(identical(other.platformTheme, platformTheme) || other.platformTheme == platformTheme)&&const DeepCollectionEquality().equals(other._recentColors, _recentColors)&&const DeepCollectionEquality().equals(other._flags, _flags)&&(identical(other.spreadPages, spreadPages) || other.spreadPages == spreadPages)&&(identical(other.highContrast, highContrast) || other.highContrast == highContrast)&&(identical(other.gridView, gridView) || other.gridView == gridView)&&(identical(other.hideExtension, hideExtension) || other.hideExtension == hideExtension)&&(identical(other.autosave, autosave) || other.autosave == autosave)&&(identical(other.showSaveButton, showSaveButton) || other.showSaveButton == showSaveButton)&&(identical(other.toolbarRows, toolbarRows) || other.toolbarRows == toolbarRows)&&(identical(other.delayedAutosave, delayedAutosave) || other.delayedAutosave == delayedAutosave)&&(identical(other.autosaveDelaySeconds, autosaveDelaySeconds) || other.autosaveDelaySeconds == autosaveDelaySeconds)&&(identical(other.hideCursorWhileDrawing, hideCursorWhileDrawing) || other.hideCursorWhileDrawing == hideCursorWhileDrawing)&&(identical(other.utilities, utilities) || other.utilities == utilities)&&(identical(other.onStartup, onStartup) || other.onStartup == onStartup)&&(identical(other.simpleToolbarVisibility, simpleToolbarVisibility) || other.simpleToolbarVisibility == simpleToolbarVisibility)&&(identical(other.optionsPanelPosition, optionsPanelPosition) || other.optionsPanelPosition == optionsPanelPosition)&&(identical(other.renderResolution, renderResolution) || other.renderResolution == renderResolution)&&(identical(other.moveOnGesture, moveOnGesture) || other.moveOnGesture == moveOnGesture)&&const DeepCollectionEquality().equals(other._swamps, _swamps)&&(identical(other.selectedPalette, selectedPalette) || other.selectedPalette == selectedPalette)&&(identical(other.showVerboseLogs, showVerboseLogs) || other.showVerboseLogs == showVerboseLogs)&&(identical(other.showThumbnails, showThumbnails) || other.showThumbnails == showThumbnails)&&(identical(other.bringMovedElementsToFront, bringMovedElementsToFront) || other.bringMovedElementsToFront == bringMovedElementsToFront)&&const DeepCollectionEquality().equals(other._favoriteTools, _favoriteTools)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ButterflySettings&&(identical(other.theme, theme) || other.theme == theme)&&(identical(other.density, density) || other.density == density)&&(identical(other.limitViewportMultiplier, limitViewportMultiplier) || other.limitViewportMultiplier == limitViewportMultiplier)&&(identical(other.limitViewportPositive, limitViewportPositive) || other.limitViewportPositive == limitViewportPositive)&&(identical(other.localeTag, localeTag) || other.localeTag == localeTag)&&(identical(other.documentPath, documentPath) || other.documentPath == documentPath)&&(identical(other.gestureSensitivity, gestureSensitivity) || other.gestureSensitivity == gestureSensitivity)&&(identical(other.touchSensitivity, touchSensitivity) || other.touchSensitivity == touchSensitivity)&&(identical(other.selectSensitivity, selectSensitivity) || other.selectSensitivity == selectSensitivity)&&(identical(other.scrollSensitivity, scrollSensitivity) || other.scrollSensitivity == scrollSensitivity)&&(identical(other.penOnlyInput, penOnlyInput) || other.penOnlyInput == penOnlyInput)&&(identical(other.showPenOnlyToggle, showPenOnlyToggle) || other.showPenOnlyToggle == showPenOnlyToggle)&&(identical(other.inputGestures, inputGestures) || other.inputGestures == inputGestures)&&(identical(other.design, design) || other.design == design)&&(identical(other.bannerVisibility, bannerVisibility) || other.bannerVisibility == bannerVisibility)&&const DeepCollectionEquality().equals(other._history, _history)&&(identical(other.zoomEnabled, zoomEnabled) || other.zoomEnabled == zoomEnabled)&&(identical(other.zoomPosition, zoomPosition) || other.zoomPosition == zoomPosition)&&(identical(other.propertyPosition, propertyPosition) || other.propertyPosition == propertyPosition)&&(identical(other.lastVersion, lastVersion) || other.lastVersion == lastVersion)&&const DeepCollectionEquality().equals(other._connections, _connections)&&(identical(other.defaultRemote, defaultRemote) || other.defaultRemote == defaultRemote)&&(identical(other.nativeTitleBar, nativeTitleBar) || other.nativeTitleBar == nativeTitleBar)&&(identical(other.startInFullScreen, startInFullScreen) || other.startInFullScreen == startInFullScreen)&&(identical(other.navigationRail, navigationRail) || other.navigationRail == navigationRail)&&(identical(other.ignorePressure, ignorePressure) || other.ignorePressure == ignorePressure)&&(identical(other.syncMode, syncMode) || other.syncMode == syncMode)&&(identical(other.inputConfiguration, inputConfiguration) || other.inputConfiguration == inputConfiguration)&&(identical(other.fallbackPack, fallbackPack) || other.fallbackPack == fallbackPack)&&const DeepCollectionEquality().equals(other._starred, _starred)&&const DeepCollectionEquality().equals(other._favoriteTemplates, _favoriteTemplates)&&(identical(other.defaultTemplate, defaultTemplate) || other.defaultTemplate == defaultTemplate)&&(identical(other.navigatorPosition, navigatorPosition) || other.navigatorPosition == navigatorPosition)&&(identical(other.toolbarPosition, toolbarPosition) || other.toolbarPosition == toolbarPosition)&&(identical(other.toolbarSize, toolbarSize) || other.toolbarSize == toolbarSize)&&(identical(other.sortBy, sortBy) || other.sortBy == sortBy)&&(identical(other.sortOrder, sortOrder) || other.sortOrder == sortOrder)&&(identical(other.imageScale, imageScale) || other.imageScale == imageScale)&&(identical(other.platformTheme, platformTheme) || other.platformTheme == platformTheme)&&const DeepCollectionEquality().equals(other._recentColors, _recentColors)&&const DeepCollectionEquality().equals(other._flags, _flags)&&(identical(other.spreadPages, spreadPages) || other.spreadPages == spreadPages)&&(identical(other.highContrast, highContrast) || other.highContrast == highContrast)&&(identical(other.gridView, gridView) || other.gridView == gridView)&&(identical(other.hideExtension, hideExtension) || other.hideExtension == hideExtension)&&(identical(other.autosave, autosave) || other.autosave == autosave)&&(identical(other.showSaveButton, showSaveButton) || other.showSaveButton == showSaveButton)&&(identical(other.toolbarRows, toolbarRows) || other.toolbarRows == toolbarRows)&&(identical(other.delayedAutosave, delayedAutosave) || other.delayedAutosave == delayedAutosave)&&(identical(other.autosaveDelaySeconds, autosaveDelaySeconds) || other.autosaveDelaySeconds == autosaveDelaySeconds)&&(identical(other.hideCursorWhileDrawing, hideCursorWhileDrawing) || other.hideCursorWhileDrawing == hideCursorWhileDrawing)&&(identical(other.onStartup, onStartup) || other.onStartup == onStartup)&&(identical(other.simpleToolbarVisibility, simpleToolbarVisibility) || other.simpleToolbarVisibility == simpleToolbarVisibility)&&(identical(other.optionsPanelPosition, optionsPanelPosition) || other.optionsPanelPosition == optionsPanelPosition)&&(identical(other.renderResolution, renderResolution) || other.renderResolution == renderResolution)&&(identical(other.moveOnGesture, moveOnGesture) || other.moveOnGesture == moveOnGesture)&&const DeepCollectionEquality().equals(other._swamps, _swamps)&&(identical(other.selectedPalette, selectedPalette) || other.selectedPalette == selectedPalette)&&(identical(other.showVerboseLogs, showVerboseLogs) || other.showVerboseLogs == showVerboseLogs)&&(identical(other.showThumbnails, showThumbnails) || other.showThumbnails == showThumbnails)&&(identical(other.bringMovedElementsToFront, bringMovedElementsToFront) || other.bringMovedElementsToFront == bringMovedElementsToFront)&&const DeepCollectionEquality().equals(other._favoriteTools, _favoriteTools)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hashAll([runtimeType,theme,density,limitViewportMultiplier,limitViewportPositive,localeTag,documentPath,gestureSensitivity,touchSensitivity,selectSensitivity,scrollSensitivity,penOnlyInput,showPenOnlyToggle,inputGestures,design,bannerVisibility,const DeepCollectionEquality().hash(_history),zoomEnabled,zoomPosition,propertyPosition,lastVersion,const DeepCollectionEquality().hash(_connections),defaultRemote,nativeTitleBar,startInFullScreen,navigationRail,ignorePressure,syncMode,inputConfiguration,fallbackPack,const DeepCollectionEquality().hash(_starred),const DeepCollectionEquality().hash(_favoriteTemplates),defaultTemplate,navigatorPosition,toolbarPosition,toolbarSize,sortBy,sortOrder,imageScale,platformTheme,const DeepCollectionEquality().hash(_recentColors),const DeepCollectionEquality().hash(_flags),spreadPages,highContrast,gridView,hideExtension,autosave,showSaveButton,toolbarRows,delayedAutosave,autosaveDelaySeconds,hideCursorWhileDrawing,utilities,onStartup,simpleToolbarVisibility,optionsPanelPosition,renderResolution,moveOnGesture,const DeepCollectionEquality().hash(_swamps),selectedPalette,showVerboseLogs,showThumbnails,bringMovedElementsToFront,const DeepCollectionEquality().hash(_favoriteTools)]); +int get hashCode => Object.hashAll([runtimeType,theme,density,limitViewportMultiplier,limitViewportPositive,localeTag,documentPath,gestureSensitivity,touchSensitivity,selectSensitivity,scrollSensitivity,penOnlyInput,showPenOnlyToggle,inputGestures,design,bannerVisibility,const DeepCollectionEquality().hash(_history),zoomEnabled,zoomPosition,propertyPosition,lastVersion,const DeepCollectionEquality().hash(_connections),defaultRemote,nativeTitleBar,startInFullScreen,navigationRail,ignorePressure,syncMode,inputConfiguration,fallbackPack,const DeepCollectionEquality().hash(_starred),const DeepCollectionEquality().hash(_favoriteTemplates),defaultTemplate,navigatorPosition,toolbarPosition,toolbarSize,sortBy,sortOrder,imageScale,platformTheme,const DeepCollectionEquality().hash(_recentColors),const DeepCollectionEquality().hash(_flags),spreadPages,highContrast,gridView,hideExtension,autosave,showSaveButton,toolbarRows,delayedAutosave,autosaveDelaySeconds,hideCursorWhileDrawing,onStartup,simpleToolbarVisibility,optionsPanelPosition,renderResolution,moveOnGesture,const DeepCollectionEquality().hash(_swamps),selectedPalette,showVerboseLogs,showThumbnails,bringMovedElementsToFront,const DeepCollectionEquality().hash(_favoriteTools)]); @override String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'ButterflySettings(theme: $theme, density: $density, limitViewportMultiplier: $limitViewportMultiplier, limitViewportPositive: $limitViewportPositive, localeTag: $localeTag, documentPath: $documentPath, gestureSensitivity: $gestureSensitivity, touchSensitivity: $touchSensitivity, selectSensitivity: $selectSensitivity, scrollSensitivity: $scrollSensitivity, penOnlyInput: $penOnlyInput, showPenOnlyToggle: $showPenOnlyToggle, inputGestures: $inputGestures, design: $design, bannerVisibility: $bannerVisibility, history: $history, zoomEnabled: $zoomEnabled, zoomPosition: $zoomPosition, propertyPosition: $propertyPosition, lastVersion: $lastVersion, connections: $connections, defaultRemote: $defaultRemote, nativeTitleBar: $nativeTitleBar, startInFullScreen: $startInFullScreen, navigationRail: $navigationRail, ignorePressure: $ignorePressure, syncMode: $syncMode, inputConfiguration: $inputConfiguration, fallbackPack: $fallbackPack, starred: $starred, favoriteTemplates: $favoriteTemplates, defaultTemplate: $defaultTemplate, navigatorPosition: $navigatorPosition, toolbarPosition: $toolbarPosition, toolbarSize: $toolbarSize, sortBy: $sortBy, sortOrder: $sortOrder, imageScale: $imageScale, platformTheme: $platformTheme, recentColors: $recentColors, flags: $flags, spreadPages: $spreadPages, highContrast: $highContrast, gridView: $gridView, hideExtension: $hideExtension, autosave: $autosave, showSaveButton: $showSaveButton, toolbarRows: $toolbarRows, delayedAutosave: $delayedAutosave, autosaveDelaySeconds: $autosaveDelaySeconds, hideCursorWhileDrawing: $hideCursorWhileDrawing, utilities: $utilities, onStartup: $onStartup, simpleToolbarVisibility: $simpleToolbarVisibility, optionsPanelPosition: $optionsPanelPosition, renderResolution: $renderResolution, moveOnGesture: $moveOnGesture, swamps: $swamps, selectedPalette: $selectedPalette, showVerboseLogs: $showVerboseLogs, showThumbnails: $showThumbnails, bringMovedElementsToFront: $bringMovedElementsToFront, favoriteTools: $favoriteTools)'; + return 'ButterflySettings(theme: $theme, density: $density, limitViewportMultiplier: $limitViewportMultiplier, limitViewportPositive: $limitViewportPositive, localeTag: $localeTag, documentPath: $documentPath, gestureSensitivity: $gestureSensitivity, touchSensitivity: $touchSensitivity, selectSensitivity: $selectSensitivity, scrollSensitivity: $scrollSensitivity, penOnlyInput: $penOnlyInput, showPenOnlyToggle: $showPenOnlyToggle, inputGestures: $inputGestures, design: $design, bannerVisibility: $bannerVisibility, history: $history, zoomEnabled: $zoomEnabled, zoomPosition: $zoomPosition, propertyPosition: $propertyPosition, lastVersion: $lastVersion, connections: $connections, defaultRemote: $defaultRemote, nativeTitleBar: $nativeTitleBar, startInFullScreen: $startInFullScreen, navigationRail: $navigationRail, ignorePressure: $ignorePressure, syncMode: $syncMode, inputConfiguration: $inputConfiguration, fallbackPack: $fallbackPack, starred: $starred, favoriteTemplates: $favoriteTemplates, defaultTemplate: $defaultTemplate, navigatorPosition: $navigatorPosition, toolbarPosition: $toolbarPosition, toolbarSize: $toolbarSize, sortBy: $sortBy, sortOrder: $sortOrder, imageScale: $imageScale, platformTheme: $platformTheme, recentColors: $recentColors, flags: $flags, spreadPages: $spreadPages, highContrast: $highContrast, gridView: $gridView, hideExtension: $hideExtension, autosave: $autosave, showSaveButton: $showSaveButton, toolbarRows: $toolbarRows, delayedAutosave: $delayedAutosave, autosaveDelaySeconds: $autosaveDelaySeconds, hideCursorWhileDrawing: $hideCursorWhileDrawing, onStartup: $onStartup, simpleToolbarVisibility: $simpleToolbarVisibility, optionsPanelPosition: $optionsPanelPosition, renderResolution: $renderResolution, moveOnGesture: $moveOnGesture, swamps: $swamps, selectedPalette: $selectedPalette, showVerboseLogs: $showVerboseLogs, showThumbnails: $showThumbnails, bringMovedElementsToFront: $bringMovedElementsToFront, favoriteTools: $favoriteTools)'; } @@ -854,11 +843,11 @@ abstract mixin class _$ButterflySettingsCopyWith<$Res> implements $ButterflySett factory _$ButterflySettingsCopyWith(_ButterflySettings value, $Res Function(_ButterflySettings) _then) = __$ButterflySettingsCopyWithImpl; @override @useResult $Res call({ - ThemeMode theme, ThemeDensity density, double? limitViewportMultiplier, bool limitViewportPositive, String localeTag, String documentPath, double gestureSensitivity, double touchSensitivity, double selectSensitivity, double scrollSensitivity, bool? penOnlyInput, bool showPenOnlyToggle, bool inputGestures, String design, BannerVisibility bannerVisibility,@JsonKey(includeFromJson: false, includeToJson: false) List history, bool zoomEnabled, ZoomPosition zoomPosition, ZoomPosition propertyPosition, String? lastVersion,@JsonKey(includeFromJson: false, includeToJson: false) List connections, String defaultRemote, bool nativeTitleBar, bool startInFullScreen, bool navigationRail, IgnorePressure ignorePressure, SyncMode syncMode, InputConfiguration inputConfiguration, String fallbackPack, List starred, List favoriteTemplates, String defaultTemplate, NavigatorPosition navigatorPosition, ToolbarPosition toolbarPosition, ToolbarSize toolbarSize, SortBy sortBy, SortOrder sortOrder, double imageScale, PlatformTheme platformTheme,@SRGBConverter() List recentColors, List flags, bool spreadPages, bool highContrast, bool gridView, bool hideExtension, bool autosave, bool showSaveButton, int toolbarRows, bool delayedAutosave, int autosaveDelaySeconds, bool hideCursorWhileDrawing, UtilitiesState utilities, StartupBehavior onStartup, SimpleToolbarVisibility simpleToolbarVisibility, OptionsPanelPosition optionsPanelPosition, RenderResolution renderResolution, bool moveOnGesture, List swamps, PackAssetLocation? selectedPalette, bool showVerboseLogs, bool showThumbnails, bool bringMovedElementsToFront, List favoriteTools + ThemeMode theme, ThemeDensity density, double? limitViewportMultiplier, bool limitViewportPositive, String localeTag, String documentPath, double gestureSensitivity, double touchSensitivity, double selectSensitivity, double scrollSensitivity, bool? penOnlyInput, bool showPenOnlyToggle, bool inputGestures, String design, BannerVisibility bannerVisibility,@JsonKey(includeFromJson: false, includeToJson: false) List history, bool zoomEnabled, ZoomPosition zoomPosition, ZoomPosition propertyPosition, String? lastVersion,@JsonKey(includeFromJson: false, includeToJson: false) List connections, String defaultRemote, bool nativeTitleBar, bool startInFullScreen, bool navigationRail, IgnorePressure ignorePressure, SyncMode syncMode, InputConfiguration inputConfiguration, String fallbackPack, List starred, List favoriteTemplates, String defaultTemplate, NavigatorPosition navigatorPosition, ToolbarPosition toolbarPosition, ToolbarSize toolbarSize, SortBy sortBy, SortOrder sortOrder, double imageScale, PlatformTheme platformTheme,@SRGBConverter() List recentColors, List flags, bool spreadPages, bool highContrast, bool gridView, bool hideExtension, bool autosave, bool showSaveButton, int toolbarRows, bool delayedAutosave, int autosaveDelaySeconds, bool hideCursorWhileDrawing, StartupBehavior onStartup, SimpleToolbarVisibility simpleToolbarVisibility, OptionsPanelPosition optionsPanelPosition, RenderResolution renderResolution, bool moveOnGesture, List swamps, PackAssetLocation? selectedPalette, bool showVerboseLogs, bool showThumbnails, bool bringMovedElementsToFront, List favoriteTools }); -@override $InputConfigurationCopyWith<$Res> get inputConfiguration;@override $UtilitiesStateCopyWith<$Res> get utilities;@override $PackAssetLocationCopyWith<$Res>? get selectedPalette; +@override $InputConfigurationCopyWith<$Res> get inputConfiguration;@override $PackAssetLocationCopyWith<$Res>? get selectedPalette; } /// @nodoc @@ -871,7 +860,7 @@ class __$ButterflySettingsCopyWithImpl<$Res> /// Create a copy of ButterflySettings /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? theme = null,Object? density = null,Object? limitViewportMultiplier = freezed,Object? limitViewportPositive = null,Object? localeTag = null,Object? documentPath = null,Object? gestureSensitivity = null,Object? touchSensitivity = null,Object? selectSensitivity = null,Object? scrollSensitivity = null,Object? penOnlyInput = freezed,Object? showPenOnlyToggle = null,Object? inputGestures = null,Object? design = null,Object? bannerVisibility = null,Object? history = null,Object? zoomEnabled = null,Object? zoomPosition = null,Object? propertyPosition = null,Object? lastVersion = freezed,Object? connections = null,Object? defaultRemote = null,Object? nativeTitleBar = null,Object? startInFullScreen = null,Object? navigationRail = null,Object? ignorePressure = null,Object? syncMode = null,Object? inputConfiguration = null,Object? fallbackPack = null,Object? starred = null,Object? favoriteTemplates = null,Object? defaultTemplate = null,Object? navigatorPosition = null,Object? toolbarPosition = null,Object? toolbarSize = null,Object? sortBy = null,Object? sortOrder = null,Object? imageScale = null,Object? platformTheme = null,Object? recentColors = null,Object? flags = null,Object? spreadPages = null,Object? highContrast = null,Object? gridView = null,Object? hideExtension = null,Object? autosave = null,Object? showSaveButton = null,Object? toolbarRows = null,Object? delayedAutosave = null,Object? autosaveDelaySeconds = null,Object? hideCursorWhileDrawing = null,Object? utilities = null,Object? onStartup = null,Object? simpleToolbarVisibility = null,Object? optionsPanelPosition = null,Object? renderResolution = null,Object? moveOnGesture = null,Object? swamps = null,Object? selectedPalette = freezed,Object? showVerboseLogs = null,Object? showThumbnails = null,Object? bringMovedElementsToFront = null,Object? favoriteTools = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? theme = null,Object? density = null,Object? limitViewportMultiplier = freezed,Object? limitViewportPositive = null,Object? localeTag = null,Object? documentPath = null,Object? gestureSensitivity = null,Object? touchSensitivity = null,Object? selectSensitivity = null,Object? scrollSensitivity = null,Object? penOnlyInput = freezed,Object? showPenOnlyToggle = null,Object? inputGestures = null,Object? design = null,Object? bannerVisibility = null,Object? history = null,Object? zoomEnabled = null,Object? zoomPosition = null,Object? propertyPosition = null,Object? lastVersion = freezed,Object? connections = null,Object? defaultRemote = null,Object? nativeTitleBar = null,Object? startInFullScreen = null,Object? navigationRail = null,Object? ignorePressure = null,Object? syncMode = null,Object? inputConfiguration = null,Object? fallbackPack = null,Object? starred = null,Object? favoriteTemplates = null,Object? defaultTemplate = null,Object? navigatorPosition = null,Object? toolbarPosition = null,Object? toolbarSize = null,Object? sortBy = null,Object? sortOrder = null,Object? imageScale = null,Object? platformTheme = null,Object? recentColors = null,Object? flags = null,Object? spreadPages = null,Object? highContrast = null,Object? gridView = null,Object? hideExtension = null,Object? autosave = null,Object? showSaveButton = null,Object? toolbarRows = null,Object? delayedAutosave = null,Object? autosaveDelaySeconds = null,Object? hideCursorWhileDrawing = null,Object? onStartup = null,Object? simpleToolbarVisibility = null,Object? optionsPanelPosition = null,Object? renderResolution = null,Object? moveOnGesture = null,Object? swamps = null,Object? selectedPalette = freezed,Object? showVerboseLogs = null,Object? showThumbnails = null,Object? bringMovedElementsToFront = null,Object? favoriteTools = null,}) { return _then(_ButterflySettings( theme: null == theme ? _self.theme : theme // ignore: cast_nullable_to_non_nullable as ThemeMode,density: null == density ? _self.density : density // ignore: cast_nullable_to_non_nullable @@ -924,8 +913,7 @@ as bool,toolbarRows: null == toolbarRows ? _self.toolbarRows : toolbarRows // ig as int,delayedAutosave: null == delayedAutosave ? _self.delayedAutosave : delayedAutosave // ignore: cast_nullable_to_non_nullable as bool,autosaveDelaySeconds: null == autosaveDelaySeconds ? _self.autosaveDelaySeconds : autosaveDelaySeconds // ignore: cast_nullable_to_non_nullable as int,hideCursorWhileDrawing: null == hideCursorWhileDrawing ? _self.hideCursorWhileDrawing : hideCursorWhileDrawing // ignore: cast_nullable_to_non_nullable -as bool,utilities: null == utilities ? _self.utilities : utilities // ignore: cast_nullable_to_non_nullable -as UtilitiesState,onStartup: null == onStartup ? _self.onStartup : onStartup // ignore: cast_nullable_to_non_nullable +as bool,onStartup: null == onStartup ? _self.onStartup : onStartup // ignore: cast_nullable_to_non_nullable as StartupBehavior,simpleToolbarVisibility: null == simpleToolbarVisibility ? _self.simpleToolbarVisibility : simpleToolbarVisibility // ignore: cast_nullable_to_non_nullable as SimpleToolbarVisibility,optionsPanelPosition: null == optionsPanelPosition ? _self.optionsPanelPosition : optionsPanelPosition // ignore: cast_nullable_to_non_nullable as OptionsPanelPosition,renderResolution: null == renderResolution ? _self.renderResolution : renderResolution // ignore: cast_nullable_to_non_nullable @@ -953,15 +941,6 @@ $InputConfigurationCopyWith<$Res> get inputConfiguration { /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') -$UtilitiesStateCopyWith<$Res> get utilities { - - return $UtilitiesStateCopyWith<$Res>(_self.utilities, (value) { - return _then(_self.copyWith(utilities: value)); - }); -}/// Create a copy of ButterflySettings -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') $PackAssetLocationCopyWith<$Res>? get selectedPalette { if (_self.selectedPalette == null) { return null; diff --git a/app/lib/cubits/settings.g.dart b/app/lib/cubits/settings.g.dart index f2c9adfe616d..1f584c24cb61 100644 --- a/app/lib/cubits/settings.g.dart +++ b/app/lib/cubits/settings.g.dart @@ -209,11 +209,6 @@ _ButterflySettings _$ButterflySettingsFromJson(Map json) => _ButterflySettings( delayedAutosave: json['delayedAutosave'] as bool? ?? true, autosaveDelaySeconds: (json['autosaveDelaySeconds'] as num?)?.toInt() ?? 3, hideCursorWhileDrawing: json['hideCursorWhileDrawing'] as bool? ?? false, - utilities: json['utilities'] == null - ? const UtilitiesState() - : UtilitiesState.fromJson( - Map.from(json['utilities'] as Map), - ), onStartup: $enumDecodeNullable(_$StartupBehaviorEnumMap, json['onStartup']) ?? StartupBehavior.openHomeScreen, @@ -314,7 +309,6 @@ Map _$ButterflySettingsToJson( 'delayedAutosave': instance.delayedAutosave, 'autosaveDelaySeconds': instance.autosaveDelaySeconds, 'hideCursorWhileDrawing': instance.hideCursorWhileDrawing, - 'utilities': instance.utilities.toJson(), 'onStartup': _$StartupBehaviorEnumMap[instance.onStartup]!, 'simpleToolbarVisibility': _$SimpleToolbarVisibilityEnumMap[instance.simpleToolbarVisibility]!, diff --git a/app/lib/cubits/transform.dart b/app/lib/cubits/transform.dart index ee28c40e3608..be23ff8c4860 100644 --- a/app/lib/cubits/transform.dart +++ b/app/lib/cubits/transform.dart @@ -435,10 +435,10 @@ class TransformCubit extends Cubit { bool force = false, Area? currentArea, }) { - final utilitiesState = runtime.viewCubit.state.utilities; + final locks = runtime.viewCubit.state.locks; if (!force) { - if (utilitiesState.lockHorizontal) delta = Offset(0, delta.dy); - if (utilitiesState.lockVertical) delta = Offset(delta.dx, 0); + if (locks.lockHorizontal) delta = Offset(0, delta.dy); + if (locks.lockVertical) delta = Offset(delta.dx, 0); final bounds = calculateViewportBounds( runtime: runtime, @@ -503,8 +503,8 @@ class TransformCubit extends Cubit { Offset cursor = Offset.zero, bool force = false, }) { - final utilitiesState = runtime.viewCubit.state.utilities; - if (utilitiesState.lockZoom && !force) { + final locks = runtime.viewCubit.state.locks; + if (locks.lockZoom && !force) { delta = 1; } if (delta == 1) { @@ -525,8 +525,8 @@ class TransformCubit extends Cubit { Offset cursor = Offset.zero, bool force = false, }) { - final utilitiesState = runtime.viewCubit.state.utilities; - if (utilitiesState.lockZoom && !force) return; + final locks = runtime.viewCubit.state.locks; + if (locks.lockZoom && !force) return; if (force) { this.size(size, cursor); return; @@ -547,17 +547,17 @@ class TransformCubit extends Cubit { }) { final settings = runtime.settingsCubit.state; if (!settings.hasFlag('smoothNavigation')) return; - final utilitiesState = runtime.viewCubit.state.utilities; + final locks = runtime.viewCubit.state.locks; Rect? bounds; var outOfBounds = false; if (!force) { - if (utilitiesState.lockHorizontal) { + if (locks.lockHorizontal) { positionVelocity = Offset(0, positionVelocity.dy); } - if (utilitiesState.lockVertical) { + if (locks.lockVertical) { positionVelocity = Offset(positionVelocity.dx, 0); } - if (utilitiesState.lockZoom) sizeVelocity = 0; + if (locks.lockZoom) sizeVelocity = 0; bounds = calculateViewportBounds( runtime: runtime, diff --git a/app/lib/handlers/eraser.dart b/app/lib/handlers/eraser.dart index 6b788787c4b1..003c3009e8c1 100644 --- a/app/lib/handlers/eraser.dart +++ b/app/lib/handlers/eraser.dart @@ -97,7 +97,7 @@ class EraserHandler extends Handler { Future _eraseAt(Offset position, EventContext context) async { final cubit = context.getEditorController(); final transform = cubit.transformCubit.state; - final utilities = cubit.viewCubit.state.utilities; + final locks = cubit.viewCubit.state.locks; final globalPos = transform.localToGlobal(position); final size = data.strokeWidth; final sizeSquared = size * size; @@ -112,8 +112,8 @@ class EraserHandler extends Handler { final ray = await context.getDocumentBloc().rayCast( globalPos, size, - useCollection: utilities.lockCollection, - useLayer: utilities.lockLayer, + useCollection: locks.lockCollection, + useLayer: locks.lockLayer, hitElementMode: data.hitElementMode, ); var elements = ray.map((e) => e.element); diff --git a/app/lib/handlers/label.dart b/app/lib/handlers/label.dart index 0348a67f777d..833cd86ae918 100644 --- a/app/lib/handlers/label.dart +++ b/app/lib/handlers/label.dart @@ -193,14 +193,14 @@ class LabelHandler extends Handler final style = theme.textTheme.bodyLarge!; if (!hit || forceCreate || _context?.element == null) { if (_context?.element != null && !hit) _submit(context.getDocumentBloc()); - final utilities = context.getViewState().utilities; + final locks = context.getViewState().locks; final hits = forceCreate ? >{} : await context.getDocumentBloc().rayCast( globalPos, 0.0, - useCollection: utilities.lockCollection, - useLayer: utilities.lockLayer, + useCollection: locks.lockCollection, + useLayer: locks.lockLayer, ); final labelRenderer = hits .whereType>() diff --git a/app/lib/handlers/polygon.dart b/app/lib/handlers/polygon.dart index 0a9ddf86066c..81b35c8b3560 100644 --- a/app/lib/handlers/polygon.dart +++ b/app/lib/handlers/polygon.dart @@ -313,15 +313,15 @@ class PolygonHandler extends Handler with ColoredHandler { final globalPos = transform.localToGlobal(localPos); if (_element == null) { - final utilities = context.getViewState().utilities; + final locks = context.getViewState().locks; final hit = await context.getDocumentBloc().rayCast( globalPos, max( 10.0 / context.getCameraTransform().size, data.property.strokeWidth / context.getCameraTransform().size * 2, ), - useCollection: utilities.lockCollection, - useLayer: utilities.lockLayer, + useCollection: locks.lockCollection, + useLayer: locks.lockLayer, ); final polygonRenderer = hit.whereType().firstOrNull; if (polygonRenderer != null) { diff --git a/app/lib/handlers/select.dart b/app/lib/handlers/select.dart index c490c058a63c..58cfe1b9598b 100644 --- a/app/lib/handlers/select.dart +++ b/app/lib/handlers/select.dart @@ -272,7 +272,7 @@ class SelectHandler extends Handler { if (_selectionManager.isTransforming) { return; } - final utilities = context.getViewState().utilities; + final locks = context.getViewState().locks; final transform = context.getCameraTransform(); final globalPos = transform.localToGlobal(localPosition); final selectionRect = getSelectionRect(); @@ -287,8 +287,8 @@ class SelectHandler extends Handler { final hits = await context.getDocumentBloc().rayCast( globalPos, radius, - useCollection: utilities.lockCollection, - useLayer: utilities.lockLayer, + useCollection: locks.lockCollection, + useLayer: locks.lockLayer, ); if (hits.isEmpty) { if (!context.isCtrlPressed) { @@ -329,12 +329,12 @@ class SelectHandler extends Handler { final bloc = context.getDocumentBloc(); final state = bloc.state; if (state is! DocumentLoadSuccess) return; - final utilities = context.getViewState().utilities; + final locks = context.getViewState().locks; final hits = await bloc.rayCast( position, 0.0, - useCollection: utilities.lockCollection, - useLayer: utilities.lockLayer, + useCollection: locks.lockCollection, + useLayer: locks.lockLayer, ); final hit = hits.firstOrNull; final rect = hit?.expandedRect; @@ -456,7 +456,7 @@ class SelectHandler extends Handler { @override void onScaleEnd(ScaleEndDetails details, EventContext context) async { - final utilities = context.getViewState().utilities; + final locks = context.getViewState().locks; final rectangleSelection = _rectangleFreeSelection?.normalized(); final lassoSelection = _lassoFreeSelection; final transformed = _submitTransform(context.getDocumentBloc()); @@ -475,16 +475,16 @@ class SelectHandler extends Handler { if (rectangleSelection != null && !rectangleSelection.isEmpty) { final hits = await context.getDocumentBloc().rayCastRect( rectangleSelection, - useCollection: utilities.lockCollection, - useLayer: utilities.lockLayer, + useCollection: locks.lockCollection, + useLayer: locks.lockLayer, hitElementMode: data.hitElementMode, ); _selected.addAll(hits); } else if (lassoSelection != null && lassoSelection.isNotEmpty) { final hits = await context.getDocumentBloc().rayCastPolygon( lassoSelection, - useCollection: utilities.lockCollection, - useLayer: utilities.lockLayer, + useCollection: locks.lockCollection, + useLayer: locks.lockLayer, hitElementMode: data.hitElementMode, ); _selected.addAll(hits); diff --git a/app/lib/models/persisted_document_state.dart b/app/lib/models/persisted_document_state.dart index 754bcf3224e0..ca8aa85513dc 100644 --- a/app/lib/models/persisted_document_state.dart +++ b/app/lib/models/persisted_document_state.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'dart:typed_data'; -import 'package:butterfly_api/butterfly_api.dart'; import 'package:crypto/crypto.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lw_file_system/lw_file_system.dart'; @@ -62,6 +61,88 @@ sealed class PersistedCameraState with _$PersistedCameraState { _$PersistedCameraStateFromJson(json); } +@freezed +sealed class PersistentLockState with _$PersistentLockState { + const PersistentLockState._(); + + const factory PersistentLockState({ + @Default(false) bool lockCollection, + @Default(false) bool lockLayer, + @Default(false) bool lockZoom, + @Default(false) bool lockHorizontal, + @Default(false) bool lockVertical, + }) = _PersistentLockState; + + factory PersistentLockState.fromJson(Map json) => + _$PersistentLockStateFromJson(json); +} + +@freezed +sealed class PersistedNavigatorState with _$PersistedNavigatorState { + const factory PersistedNavigatorState({ + @Default(false) bool enabled, + @Default('waypoints') String page, + }) = _PersistedNavigatorState; + + factory PersistedNavigatorState.fromJson(Map json) => + _$PersistedNavigatorStateFromJson(json); +} + +@freezed +sealed class PersistedLayerState with _$PersistedLayerState { + const factory PersistedLayerState({ + @Default('') String currentLayer, + @Default('') String currentCollection, + @Default({}) Set invisibleLayers, + }) = _PersistedLayerState; + + factory PersistedLayerState.fromJson(Map json) => + _$PersistedLayerStateFromJson(json); +} + +@freezed +sealed class PersistedAreaNavigatorState with _$PersistedAreaNavigatorState { + const factory PersistedAreaNavigatorState({ + @Default(true) bool create, + @Default(true) bool exact, + @Default(false) bool ask, + }) = _PersistedAreaNavigatorState; + + factory PersistedAreaNavigatorState.fromJson(Map json) => + _$PersistedAreaNavigatorStateFromJson(json); +} + +Object? _readLocks(Map json, String key) => json[key] ?? json['utilities']; + +Object? _readNavigator(Map json, String key) => + json[key] ?? + { + if (json.containsKey('navigatorEnabled')) + 'enabled': json['navigatorEnabled'], + if (json.containsKey('navigatorPage')) 'page': json['navigatorPage'], + }; + +Object? _readLayers(Map json, String key) => + json[key] ?? + { + if (json.containsKey('currentLayer')) + 'currentLayer': json['currentLayer'], + if (json.containsKey('currentCollection')) + 'currentCollection': json['currentCollection'], + if (json.containsKey('invisibleLayers')) + 'invisibleLayers': json['invisibleLayers'], + }; + +Object? _readAreaNavigator(Map json, String key) => + json[key] ?? + { + if (json.containsKey('areaNavigatorCreate')) + 'create': json['areaNavigatorCreate'], + if (json.containsKey('areaNavigatorExact')) + 'exact': json['areaNavigatorExact'], + if (json.containsKey('areaNavigatorAsk')) 'ask': json['areaNavigatorAsk'], + }; + @freezed sealed class PersistedDocumentState with _$PersistedDocumentState { const PersistedDocumentState._(); @@ -72,16 +153,19 @@ sealed class PersistedDocumentState with _$PersistedDocumentState { String? contentHash, String? pageName, @Default(PersistedCameraState()) PersistedCameraState camera, - @Default(UtilitiesState()) UtilitiesState utilities, + @JsonKey(readValue: _readLocks) + @Default(PersistentLockState()) + PersistentLockState locks, @Default(PersistedToolSelection()) PersistedToolSelection selectedTool, - @Default(false) bool navigatorEnabled, - @Default('waypoints') String navigatorPage, - @Default('') String currentLayer, - @Default('') String currentCollection, - @Default({}) Set invisibleLayers, - @Default(true) bool areaNavigatorCreate, - @Default(true) bool areaNavigatorExact, - @Default(false) bool areaNavigatorAsk, + @JsonKey(readValue: _readNavigator) + @Default(PersistedNavigatorState()) + PersistedNavigatorState navigator, + @JsonKey(readValue: _readLayers) + @Default(PersistedLayerState()) + PersistedLayerState layers, + @JsonKey(readValue: _readAreaNavigator) + @Default(PersistedAreaNavigatorState()) + PersistedAreaNavigatorState areaNavigator, DateTime? updatedAt, }) = _PersistedDocumentState; diff --git a/app/lib/models/persisted_document_state.freezed.dart b/app/lib/models/persisted_document_state.freezed.dart index 2c70e1c440f3..18c4e5712b50 100644 --- a/app/lib/models/persisted_document_state.freezed.dart +++ b/app/lib/models/persisted_document_state.freezed.dart @@ -289,10 +289,579 @@ as double, } +/// @nodoc +mixin _$PersistentLockState { + + bool get lockCollection; bool get lockLayer; bool get lockZoom; bool get lockHorizontal; bool get lockVertical; +/// Create a copy of PersistentLockState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$PersistentLockStateCopyWith get copyWith => _$PersistentLockStateCopyWithImpl(this as PersistentLockState, _$identity); + + /// Serializes this PersistentLockState to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is PersistentLockState&&(identical(other.lockCollection, lockCollection) || other.lockCollection == lockCollection)&&(identical(other.lockLayer, lockLayer) || other.lockLayer == lockLayer)&&(identical(other.lockZoom, lockZoom) || other.lockZoom == lockZoom)&&(identical(other.lockHorizontal, lockHorizontal) || other.lockHorizontal == lockHorizontal)&&(identical(other.lockVertical, lockVertical) || other.lockVertical == lockVertical)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,lockCollection,lockLayer,lockZoom,lockHorizontal,lockVertical); + +@override +String toString() { + return 'PersistentLockState(lockCollection: $lockCollection, lockLayer: $lockLayer, lockZoom: $lockZoom, lockHorizontal: $lockHorizontal, lockVertical: $lockVertical)'; +} + + +} + +/// @nodoc +abstract mixin class $PersistentLockStateCopyWith<$Res> { + factory $PersistentLockStateCopyWith(PersistentLockState value, $Res Function(PersistentLockState) _then) = _$PersistentLockStateCopyWithImpl; +@useResult +$Res call({ + bool lockCollection, bool lockLayer, bool lockZoom, bool lockHorizontal, bool lockVertical +}); + + + + +} +/// @nodoc +class _$PersistentLockStateCopyWithImpl<$Res> + implements $PersistentLockStateCopyWith<$Res> { + _$PersistentLockStateCopyWithImpl(this._self, this._then); + + final PersistentLockState _self; + final $Res Function(PersistentLockState) _then; + +/// Create a copy of PersistentLockState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? lockCollection = null,Object? lockLayer = null,Object? lockZoom = null,Object? lockHorizontal = null,Object? lockVertical = null,}) { + return _then(_self.copyWith( +lockCollection: null == lockCollection ? _self.lockCollection : lockCollection // ignore: cast_nullable_to_non_nullable +as bool,lockLayer: null == lockLayer ? _self.lockLayer : lockLayer // ignore: cast_nullable_to_non_nullable +as bool,lockZoom: null == lockZoom ? _self.lockZoom : lockZoom // ignore: cast_nullable_to_non_nullable +as bool,lockHorizontal: null == lockHorizontal ? _self.lockHorizontal : lockHorizontal // ignore: cast_nullable_to_non_nullable +as bool,lockVertical: null == lockVertical ? _self.lockVertical : lockVertical // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + + +/// @nodoc +@JsonSerializable() + +class _PersistentLockState extends PersistentLockState { + const _PersistentLockState({this.lockCollection = false, this.lockLayer = false, this.lockZoom = false, this.lockHorizontal = false, this.lockVertical = false}): super._(); + factory _PersistentLockState.fromJson(Map json) => _$PersistentLockStateFromJson(json); + +@override@JsonKey() final bool lockCollection; +@override@JsonKey() final bool lockLayer; +@override@JsonKey() final bool lockZoom; +@override@JsonKey() final bool lockHorizontal; +@override@JsonKey() final bool lockVertical; + +/// Create a copy of PersistentLockState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$PersistentLockStateCopyWith<_PersistentLockState> get copyWith => __$PersistentLockStateCopyWithImpl<_PersistentLockState>(this, _$identity); + +@override +Map toJson() { + return _$PersistentLockStateToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _PersistentLockState&&(identical(other.lockCollection, lockCollection) || other.lockCollection == lockCollection)&&(identical(other.lockLayer, lockLayer) || other.lockLayer == lockLayer)&&(identical(other.lockZoom, lockZoom) || other.lockZoom == lockZoom)&&(identical(other.lockHorizontal, lockHorizontal) || other.lockHorizontal == lockHorizontal)&&(identical(other.lockVertical, lockVertical) || other.lockVertical == lockVertical)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,lockCollection,lockLayer,lockZoom,lockHorizontal,lockVertical); + +@override +String toString() { + return 'PersistentLockState(lockCollection: $lockCollection, lockLayer: $lockLayer, lockZoom: $lockZoom, lockHorizontal: $lockHorizontal, lockVertical: $lockVertical)'; +} + + +} + +/// @nodoc +abstract mixin class _$PersistentLockStateCopyWith<$Res> implements $PersistentLockStateCopyWith<$Res> { + factory _$PersistentLockStateCopyWith(_PersistentLockState value, $Res Function(_PersistentLockState) _then) = __$PersistentLockStateCopyWithImpl; +@override @useResult +$Res call({ + bool lockCollection, bool lockLayer, bool lockZoom, bool lockHorizontal, bool lockVertical +}); + + + + +} +/// @nodoc +class __$PersistentLockStateCopyWithImpl<$Res> + implements _$PersistentLockStateCopyWith<$Res> { + __$PersistentLockStateCopyWithImpl(this._self, this._then); + + final _PersistentLockState _self; + final $Res Function(_PersistentLockState) _then; + +/// Create a copy of PersistentLockState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? lockCollection = null,Object? lockLayer = null,Object? lockZoom = null,Object? lockHorizontal = null,Object? lockVertical = null,}) { + return _then(_PersistentLockState( +lockCollection: null == lockCollection ? _self.lockCollection : lockCollection // ignore: cast_nullable_to_non_nullable +as bool,lockLayer: null == lockLayer ? _self.lockLayer : lockLayer // ignore: cast_nullable_to_non_nullable +as bool,lockZoom: null == lockZoom ? _self.lockZoom : lockZoom // ignore: cast_nullable_to_non_nullable +as bool,lockHorizontal: null == lockHorizontal ? _self.lockHorizontal : lockHorizontal // ignore: cast_nullable_to_non_nullable +as bool,lockVertical: null == lockVertical ? _self.lockVertical : lockVertical // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + + +/// @nodoc +mixin _$PersistedNavigatorState { + + bool get enabled; String get page; +/// Create a copy of PersistedNavigatorState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$PersistedNavigatorStateCopyWith get copyWith => _$PersistedNavigatorStateCopyWithImpl(this as PersistedNavigatorState, _$identity); + + /// Serializes this PersistedNavigatorState to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is PersistedNavigatorState&&(identical(other.enabled, enabled) || other.enabled == enabled)&&(identical(other.page, page) || other.page == page)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,enabled,page); + +@override +String toString() { + return 'PersistedNavigatorState(enabled: $enabled, page: $page)'; +} + + +} + +/// @nodoc +abstract mixin class $PersistedNavigatorStateCopyWith<$Res> { + factory $PersistedNavigatorStateCopyWith(PersistedNavigatorState value, $Res Function(PersistedNavigatorState) _then) = _$PersistedNavigatorStateCopyWithImpl; +@useResult +$Res call({ + bool enabled, String page +}); + + + + +} +/// @nodoc +class _$PersistedNavigatorStateCopyWithImpl<$Res> + implements $PersistedNavigatorStateCopyWith<$Res> { + _$PersistedNavigatorStateCopyWithImpl(this._self, this._then); + + final PersistedNavigatorState _self; + final $Res Function(PersistedNavigatorState) _then; + +/// Create a copy of PersistedNavigatorState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? enabled = null,Object? page = null,}) { + return _then(_self.copyWith( +enabled: null == enabled ? _self.enabled : enabled // ignore: cast_nullable_to_non_nullable +as bool,page: null == page ? _self.page : page // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + + +/// @nodoc +@JsonSerializable() + +class _PersistedNavigatorState implements PersistedNavigatorState { + const _PersistedNavigatorState({this.enabled = false, this.page = 'waypoints'}); + factory _PersistedNavigatorState.fromJson(Map json) => _$PersistedNavigatorStateFromJson(json); + +@override@JsonKey() final bool enabled; +@override@JsonKey() final String page; + +/// Create a copy of PersistedNavigatorState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$PersistedNavigatorStateCopyWith<_PersistedNavigatorState> get copyWith => __$PersistedNavigatorStateCopyWithImpl<_PersistedNavigatorState>(this, _$identity); + +@override +Map toJson() { + return _$PersistedNavigatorStateToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _PersistedNavigatorState&&(identical(other.enabled, enabled) || other.enabled == enabled)&&(identical(other.page, page) || other.page == page)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,enabled,page); + +@override +String toString() { + return 'PersistedNavigatorState(enabled: $enabled, page: $page)'; +} + + +} + +/// @nodoc +abstract mixin class _$PersistedNavigatorStateCopyWith<$Res> implements $PersistedNavigatorStateCopyWith<$Res> { + factory _$PersistedNavigatorStateCopyWith(_PersistedNavigatorState value, $Res Function(_PersistedNavigatorState) _then) = __$PersistedNavigatorStateCopyWithImpl; +@override @useResult +$Res call({ + bool enabled, String page +}); + + + + +} +/// @nodoc +class __$PersistedNavigatorStateCopyWithImpl<$Res> + implements _$PersistedNavigatorStateCopyWith<$Res> { + __$PersistedNavigatorStateCopyWithImpl(this._self, this._then); + + final _PersistedNavigatorState _self; + final $Res Function(_PersistedNavigatorState) _then; + +/// Create a copy of PersistedNavigatorState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? enabled = null,Object? page = null,}) { + return _then(_PersistedNavigatorState( +enabled: null == enabled ? _self.enabled : enabled // ignore: cast_nullable_to_non_nullable +as bool,page: null == page ? _self.page : page // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + + +/// @nodoc +mixin _$PersistedLayerState { + + String get currentLayer; String get currentCollection; Set get invisibleLayers; +/// Create a copy of PersistedLayerState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$PersistedLayerStateCopyWith get copyWith => _$PersistedLayerStateCopyWithImpl(this as PersistedLayerState, _$identity); + + /// Serializes this PersistedLayerState to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is PersistedLayerState&&(identical(other.currentLayer, currentLayer) || other.currentLayer == currentLayer)&&(identical(other.currentCollection, currentCollection) || other.currentCollection == currentCollection)&&const DeepCollectionEquality().equals(other.invisibleLayers, invisibleLayers)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,currentLayer,currentCollection,const DeepCollectionEquality().hash(invisibleLayers)); + +@override +String toString() { + return 'PersistedLayerState(currentLayer: $currentLayer, currentCollection: $currentCollection, invisibleLayers: $invisibleLayers)'; +} + + +} + +/// @nodoc +abstract mixin class $PersistedLayerStateCopyWith<$Res> { + factory $PersistedLayerStateCopyWith(PersistedLayerState value, $Res Function(PersistedLayerState) _then) = _$PersistedLayerStateCopyWithImpl; +@useResult +$Res call({ + String currentLayer, String currentCollection, Set invisibleLayers +}); + + + + +} +/// @nodoc +class _$PersistedLayerStateCopyWithImpl<$Res> + implements $PersistedLayerStateCopyWith<$Res> { + _$PersistedLayerStateCopyWithImpl(this._self, this._then); + + final PersistedLayerState _self; + final $Res Function(PersistedLayerState) _then; + +/// Create a copy of PersistedLayerState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? currentLayer = null,Object? currentCollection = null,Object? invisibleLayers = null,}) { + return _then(_self.copyWith( +currentLayer: null == currentLayer ? _self.currentLayer : currentLayer // ignore: cast_nullable_to_non_nullable +as String,currentCollection: null == currentCollection ? _self.currentCollection : currentCollection // ignore: cast_nullable_to_non_nullable +as String,invisibleLayers: null == invisibleLayers ? _self.invisibleLayers : invisibleLayers // ignore: cast_nullable_to_non_nullable +as Set, + )); +} + +} + + + +/// @nodoc +@JsonSerializable() + +class _PersistedLayerState implements PersistedLayerState { + const _PersistedLayerState({this.currentLayer = '', this.currentCollection = '', final Set invisibleLayers = const {}}): _invisibleLayers = invisibleLayers; + factory _PersistedLayerState.fromJson(Map json) => _$PersistedLayerStateFromJson(json); + +@override@JsonKey() final String currentLayer; +@override@JsonKey() final String currentCollection; + final Set _invisibleLayers; +@override@JsonKey() Set get invisibleLayers { + if (_invisibleLayers is EqualUnmodifiableSetView) return _invisibleLayers; + // ignore: implicit_dynamic_type + return EqualUnmodifiableSetView(_invisibleLayers); +} + + +/// Create a copy of PersistedLayerState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$PersistedLayerStateCopyWith<_PersistedLayerState> get copyWith => __$PersistedLayerStateCopyWithImpl<_PersistedLayerState>(this, _$identity); + +@override +Map toJson() { + return _$PersistedLayerStateToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _PersistedLayerState&&(identical(other.currentLayer, currentLayer) || other.currentLayer == currentLayer)&&(identical(other.currentCollection, currentCollection) || other.currentCollection == currentCollection)&&const DeepCollectionEquality().equals(other._invisibleLayers, _invisibleLayers)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,currentLayer,currentCollection,const DeepCollectionEquality().hash(_invisibleLayers)); + +@override +String toString() { + return 'PersistedLayerState(currentLayer: $currentLayer, currentCollection: $currentCollection, invisibleLayers: $invisibleLayers)'; +} + + +} + +/// @nodoc +abstract mixin class _$PersistedLayerStateCopyWith<$Res> implements $PersistedLayerStateCopyWith<$Res> { + factory _$PersistedLayerStateCopyWith(_PersistedLayerState value, $Res Function(_PersistedLayerState) _then) = __$PersistedLayerStateCopyWithImpl; +@override @useResult +$Res call({ + String currentLayer, String currentCollection, Set invisibleLayers +}); + + + + +} +/// @nodoc +class __$PersistedLayerStateCopyWithImpl<$Res> + implements _$PersistedLayerStateCopyWith<$Res> { + __$PersistedLayerStateCopyWithImpl(this._self, this._then); + + final _PersistedLayerState _self; + final $Res Function(_PersistedLayerState) _then; + +/// Create a copy of PersistedLayerState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? currentLayer = null,Object? currentCollection = null,Object? invisibleLayers = null,}) { + return _then(_PersistedLayerState( +currentLayer: null == currentLayer ? _self.currentLayer : currentLayer // ignore: cast_nullable_to_non_nullable +as String,currentCollection: null == currentCollection ? _self.currentCollection : currentCollection // ignore: cast_nullable_to_non_nullable +as String,invisibleLayers: null == invisibleLayers ? _self._invisibleLayers : invisibleLayers // ignore: cast_nullable_to_non_nullable +as Set, + )); +} + + +} + + +/// @nodoc +mixin _$PersistedAreaNavigatorState { + + bool get create; bool get exact; bool get ask; +/// Create a copy of PersistedAreaNavigatorState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$PersistedAreaNavigatorStateCopyWith get copyWith => _$PersistedAreaNavigatorStateCopyWithImpl(this as PersistedAreaNavigatorState, _$identity); + + /// Serializes this PersistedAreaNavigatorState to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is PersistedAreaNavigatorState&&(identical(other.create, create) || other.create == create)&&(identical(other.exact, exact) || other.exact == exact)&&(identical(other.ask, ask) || other.ask == ask)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,create,exact,ask); + +@override +String toString() { + return 'PersistedAreaNavigatorState(create: $create, exact: $exact, ask: $ask)'; +} + + +} + +/// @nodoc +abstract mixin class $PersistedAreaNavigatorStateCopyWith<$Res> { + factory $PersistedAreaNavigatorStateCopyWith(PersistedAreaNavigatorState value, $Res Function(PersistedAreaNavigatorState) _then) = _$PersistedAreaNavigatorStateCopyWithImpl; +@useResult +$Res call({ + bool create, bool exact, bool ask +}); + + + + +} +/// @nodoc +class _$PersistedAreaNavigatorStateCopyWithImpl<$Res> + implements $PersistedAreaNavigatorStateCopyWith<$Res> { + _$PersistedAreaNavigatorStateCopyWithImpl(this._self, this._then); + + final PersistedAreaNavigatorState _self; + final $Res Function(PersistedAreaNavigatorState) _then; + +/// Create a copy of PersistedAreaNavigatorState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? create = null,Object? exact = null,Object? ask = null,}) { + return _then(_self.copyWith( +create: null == create ? _self.create : create // ignore: cast_nullable_to_non_nullable +as bool,exact: null == exact ? _self.exact : exact // ignore: cast_nullable_to_non_nullable +as bool,ask: null == ask ? _self.ask : ask // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + + +/// @nodoc +@JsonSerializable() + +class _PersistedAreaNavigatorState implements PersistedAreaNavigatorState { + const _PersistedAreaNavigatorState({this.create = true, this.exact = true, this.ask = false}); + factory _PersistedAreaNavigatorState.fromJson(Map json) => _$PersistedAreaNavigatorStateFromJson(json); + +@override@JsonKey() final bool create; +@override@JsonKey() final bool exact; +@override@JsonKey() final bool ask; + +/// Create a copy of PersistedAreaNavigatorState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$PersistedAreaNavigatorStateCopyWith<_PersistedAreaNavigatorState> get copyWith => __$PersistedAreaNavigatorStateCopyWithImpl<_PersistedAreaNavigatorState>(this, _$identity); + +@override +Map toJson() { + return _$PersistedAreaNavigatorStateToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _PersistedAreaNavigatorState&&(identical(other.create, create) || other.create == create)&&(identical(other.exact, exact) || other.exact == exact)&&(identical(other.ask, ask) || other.ask == ask)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,create,exact,ask); + +@override +String toString() { + return 'PersistedAreaNavigatorState(create: $create, exact: $exact, ask: $ask)'; +} + + +} + +/// @nodoc +abstract mixin class _$PersistedAreaNavigatorStateCopyWith<$Res> implements $PersistedAreaNavigatorStateCopyWith<$Res> { + factory _$PersistedAreaNavigatorStateCopyWith(_PersistedAreaNavigatorState value, $Res Function(_PersistedAreaNavigatorState) _then) = __$PersistedAreaNavigatorStateCopyWithImpl; +@override @useResult +$Res call({ + bool create, bool exact, bool ask +}); + + + + +} +/// @nodoc +class __$PersistedAreaNavigatorStateCopyWithImpl<$Res> + implements _$PersistedAreaNavigatorStateCopyWith<$Res> { + __$PersistedAreaNavigatorStateCopyWithImpl(this._self, this._then); + + final _PersistedAreaNavigatorState _self; + final $Res Function(_PersistedAreaNavigatorState) _then; + +/// Create a copy of PersistedAreaNavigatorState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? create = null,Object? exact = null,Object? ask = null,}) { + return _then(_PersistedAreaNavigatorState( +create: null == create ? _self.create : create // ignore: cast_nullable_to_non_nullable +as bool,exact: null == exact ? _self.exact : exact // ignore: cast_nullable_to_non_nullable +as bool,ask: null == ask ? _self.ask : ask // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + + /// @nodoc mixin _$PersistedDocumentState { - int get version; String? get pathKey; String? get contentHash; String? get pageName; PersistedCameraState get camera; UtilitiesState get utilities; PersistedToolSelection get selectedTool; bool get navigatorEnabled; String get navigatorPage; String get currentLayer; String get currentCollection; Set get invisibleLayers; bool get areaNavigatorCreate; bool get areaNavigatorExact; bool get areaNavigatorAsk; DateTime? get updatedAt; + int get version; String? get pathKey; String? get contentHash; String? get pageName; PersistedCameraState get camera;@JsonKey(readValue: _readLocks) PersistentLockState get locks; PersistedToolSelection get selectedTool;@JsonKey(readValue: _readNavigator) PersistedNavigatorState get navigator;@JsonKey(readValue: _readLayers) PersistedLayerState get layers;@JsonKey(readValue: _readAreaNavigator) PersistedAreaNavigatorState get areaNavigator; DateTime? get updatedAt; /// Create a copy of PersistedDocumentState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -305,16 +874,16 @@ $PersistedDocumentStateCopyWith get copyWith => _$Persis @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is PersistedDocumentState&&(identical(other.version, version) || other.version == version)&&(identical(other.pathKey, pathKey) || other.pathKey == pathKey)&&(identical(other.contentHash, contentHash) || other.contentHash == contentHash)&&(identical(other.pageName, pageName) || other.pageName == pageName)&&(identical(other.camera, camera) || other.camera == camera)&&(identical(other.utilities, utilities) || other.utilities == utilities)&&(identical(other.selectedTool, selectedTool) || other.selectedTool == selectedTool)&&(identical(other.navigatorEnabled, navigatorEnabled) || other.navigatorEnabled == navigatorEnabled)&&(identical(other.navigatorPage, navigatorPage) || other.navigatorPage == navigatorPage)&&(identical(other.currentLayer, currentLayer) || other.currentLayer == currentLayer)&&(identical(other.currentCollection, currentCollection) || other.currentCollection == currentCollection)&&const DeepCollectionEquality().equals(other.invisibleLayers, invisibleLayers)&&(identical(other.areaNavigatorCreate, areaNavigatorCreate) || other.areaNavigatorCreate == areaNavigatorCreate)&&(identical(other.areaNavigatorExact, areaNavigatorExact) || other.areaNavigatorExact == areaNavigatorExact)&&(identical(other.areaNavigatorAsk, areaNavigatorAsk) || other.areaNavigatorAsk == areaNavigatorAsk)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is PersistedDocumentState&&(identical(other.version, version) || other.version == version)&&(identical(other.pathKey, pathKey) || other.pathKey == pathKey)&&(identical(other.contentHash, contentHash) || other.contentHash == contentHash)&&(identical(other.pageName, pageName) || other.pageName == pageName)&&(identical(other.camera, camera) || other.camera == camera)&&(identical(other.locks, locks) || other.locks == locks)&&(identical(other.selectedTool, selectedTool) || other.selectedTool == selectedTool)&&(identical(other.navigator, navigator) || other.navigator == navigator)&&(identical(other.layers, layers) || other.layers == layers)&&(identical(other.areaNavigator, areaNavigator) || other.areaNavigator == areaNavigator)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,version,pathKey,contentHash,pageName,camera,utilities,selectedTool,navigatorEnabled,navigatorPage,currentLayer,currentCollection,const DeepCollectionEquality().hash(invisibleLayers),areaNavigatorCreate,areaNavigatorExact,areaNavigatorAsk,updatedAt); +int get hashCode => Object.hash(runtimeType,version,pathKey,contentHash,pageName,camera,locks,selectedTool,navigator,layers,areaNavigator,updatedAt); @override String toString() { - return 'PersistedDocumentState(version: $version, pathKey: $pathKey, contentHash: $contentHash, pageName: $pageName, camera: $camera, utilities: $utilities, selectedTool: $selectedTool, navigatorEnabled: $navigatorEnabled, navigatorPage: $navigatorPage, currentLayer: $currentLayer, currentCollection: $currentCollection, invisibleLayers: $invisibleLayers, areaNavigatorCreate: $areaNavigatorCreate, areaNavigatorExact: $areaNavigatorExact, areaNavigatorAsk: $areaNavigatorAsk, updatedAt: $updatedAt)'; + return 'PersistedDocumentState(version: $version, pathKey: $pathKey, contentHash: $contentHash, pageName: $pageName, camera: $camera, locks: $locks, selectedTool: $selectedTool, navigator: $navigator, layers: $layers, areaNavigator: $areaNavigator, updatedAt: $updatedAt)'; } @@ -325,11 +894,11 @@ abstract mixin class $PersistedDocumentStateCopyWith<$Res> { factory $PersistedDocumentStateCopyWith(PersistedDocumentState value, $Res Function(PersistedDocumentState) _then) = _$PersistedDocumentStateCopyWithImpl; @useResult $Res call({ - int version, String? pathKey, String? contentHash, String? pageName, PersistedCameraState camera, UtilitiesState utilities, PersistedToolSelection selectedTool, bool navigatorEnabled, String navigatorPage, String currentLayer, String currentCollection, Set invisibleLayers, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, DateTime? updatedAt + int version, String? pathKey, String? contentHash, String? pageName, PersistedCameraState camera,@JsonKey(readValue: _readLocks) PersistentLockState locks, PersistedToolSelection selectedTool,@JsonKey(readValue: _readNavigator) PersistedNavigatorState navigator,@JsonKey(readValue: _readLayers) PersistedLayerState layers,@JsonKey(readValue: _readAreaNavigator) PersistedAreaNavigatorState areaNavigator, DateTime? updatedAt }); -$PersistedCameraStateCopyWith<$Res> get camera;$UtilitiesStateCopyWith<$Res> get utilities;$PersistedToolSelectionCopyWith<$Res> get selectedTool; +$PersistedCameraStateCopyWith<$Res> get camera;$PersistentLockStateCopyWith<$Res> get locks;$PersistedToolSelectionCopyWith<$Res> get selectedTool;$PersistedNavigatorStateCopyWith<$Res> get navigator;$PersistedLayerStateCopyWith<$Res> get layers;$PersistedAreaNavigatorStateCopyWith<$Res> get areaNavigator; } /// @nodoc @@ -342,24 +911,19 @@ class _$PersistedDocumentStateCopyWithImpl<$Res> /// Create a copy of PersistedDocumentState /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? version = null,Object? pathKey = freezed,Object? contentHash = freezed,Object? pageName = freezed,Object? camera = null,Object? utilities = null,Object? selectedTool = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? currentLayer = null,Object? currentCollection = null,Object? invisibleLayers = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? updatedAt = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? version = null,Object? pathKey = freezed,Object? contentHash = freezed,Object? pageName = freezed,Object? camera = null,Object? locks = null,Object? selectedTool = null,Object? navigator = null,Object? layers = null,Object? areaNavigator = null,Object? updatedAt = freezed,}) { return _then(_self.copyWith( version: null == version ? _self.version : version // ignore: cast_nullable_to_non_nullable as int,pathKey: freezed == pathKey ? _self.pathKey : pathKey // ignore: cast_nullable_to_non_nullable as String?,contentHash: freezed == contentHash ? _self.contentHash : contentHash // ignore: cast_nullable_to_non_nullable as String?,pageName: freezed == pageName ? _self.pageName : pageName // ignore: cast_nullable_to_non_nullable as String?,camera: null == camera ? _self.camera : camera // ignore: cast_nullable_to_non_nullable -as PersistedCameraState,utilities: null == utilities ? _self.utilities : utilities // ignore: cast_nullable_to_non_nullable -as UtilitiesState,selectedTool: null == selectedTool ? _self.selectedTool : selectedTool // ignore: cast_nullable_to_non_nullable -as PersistedToolSelection,navigatorEnabled: null == navigatorEnabled ? _self.navigatorEnabled : navigatorEnabled // ignore: cast_nullable_to_non_nullable -as bool,navigatorPage: null == navigatorPage ? _self.navigatorPage : navigatorPage // ignore: cast_nullable_to_non_nullable -as String,currentLayer: null == currentLayer ? _self.currentLayer : currentLayer // ignore: cast_nullable_to_non_nullable -as String,currentCollection: null == currentCollection ? _self.currentCollection : currentCollection // ignore: cast_nullable_to_non_nullable -as String,invisibleLayers: null == invisibleLayers ? _self.invisibleLayers : invisibleLayers // ignore: cast_nullable_to_non_nullable -as Set,areaNavigatorCreate: null == areaNavigatorCreate ? _self.areaNavigatorCreate : areaNavigatorCreate // ignore: cast_nullable_to_non_nullable -as bool,areaNavigatorExact: null == areaNavigatorExact ? _self.areaNavigatorExact : areaNavigatorExact // ignore: cast_nullable_to_non_nullable -as bool,areaNavigatorAsk: null == areaNavigatorAsk ? _self.areaNavigatorAsk : areaNavigatorAsk // ignore: cast_nullable_to_non_nullable -as bool,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable +as PersistedCameraState,locks: null == locks ? _self.locks : locks // ignore: cast_nullable_to_non_nullable +as PersistentLockState,selectedTool: null == selectedTool ? _self.selectedTool : selectedTool // ignore: cast_nullable_to_non_nullable +as PersistedToolSelection,navigator: null == navigator ? _self.navigator : navigator // ignore: cast_nullable_to_non_nullable +as PersistedNavigatorState,layers: null == layers ? _self.layers : layers // ignore: cast_nullable_to_non_nullable +as PersistedLayerState,areaNavigator: null == areaNavigator ? _self.areaNavigator : areaNavigator // ignore: cast_nullable_to_non_nullable +as PersistedAreaNavigatorState,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime?, )); } @@ -376,10 +940,10 @@ $PersistedCameraStateCopyWith<$Res> get camera { /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') -$UtilitiesStateCopyWith<$Res> get utilities { +$PersistentLockStateCopyWith<$Res> get locks { - return $UtilitiesStateCopyWith<$Res>(_self.utilities, (value) { - return _then(_self.copyWith(utilities: value)); + return $PersistentLockStateCopyWith<$Res>(_self.locks, (value) { + return _then(_self.copyWith(locks: value)); }); }/// Create a copy of PersistedDocumentState /// with the given fields replaced by the non-null parameter values. @@ -390,6 +954,33 @@ $PersistedToolSelectionCopyWith<$Res> get selectedTool { return $PersistedToolSelectionCopyWith<$Res>(_self.selectedTool, (value) { return _then(_self.copyWith(selectedTool: value)); }); +}/// Create a copy of PersistedDocumentState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$PersistedNavigatorStateCopyWith<$Res> get navigator { + + return $PersistedNavigatorStateCopyWith<$Res>(_self.navigator, (value) { + return _then(_self.copyWith(navigator: value)); + }); +}/// Create a copy of PersistedDocumentState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$PersistedLayerStateCopyWith<$Res> get layers { + + return $PersistedLayerStateCopyWith<$Res>(_self.layers, (value) { + return _then(_self.copyWith(layers: value)); + }); +}/// Create a copy of PersistedDocumentState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$PersistedAreaNavigatorStateCopyWith<$Res> get areaNavigator { + + return $PersistedAreaNavigatorStateCopyWith<$Res>(_self.areaNavigator, (value) { + return _then(_self.copyWith(areaNavigator: value)); + }); } } @@ -399,7 +990,7 @@ $PersistedToolSelectionCopyWith<$Res> get selectedTool { @JsonSerializable() class _PersistedDocumentState extends PersistedDocumentState { - const _PersistedDocumentState({this.version = kPersistedDocumentStateVersion, this.pathKey, this.contentHash, this.pageName, this.camera = const PersistedCameraState(), this.utilities = const UtilitiesState(), this.selectedTool = const PersistedToolSelection(), this.navigatorEnabled = false, this.navigatorPage = 'waypoints', this.currentLayer = '', this.currentCollection = '', final Set invisibleLayers = const {}, this.areaNavigatorCreate = true, this.areaNavigatorExact = true, this.areaNavigatorAsk = false, this.updatedAt}): _invisibleLayers = invisibleLayers,super._(); + const _PersistedDocumentState({this.version = kPersistedDocumentStateVersion, this.pathKey, this.contentHash, this.pageName, this.camera = const PersistedCameraState(), @JsonKey(readValue: _readLocks) this.locks = const PersistentLockState(), this.selectedTool = const PersistedToolSelection(), @JsonKey(readValue: _readNavigator) this.navigator = const PersistedNavigatorState(), @JsonKey(readValue: _readLayers) this.layers = const PersistedLayerState(), @JsonKey(readValue: _readAreaNavigator) this.areaNavigator = const PersistedAreaNavigatorState(), this.updatedAt}): super._(); factory _PersistedDocumentState.fromJson(Map json) => _$PersistedDocumentStateFromJson(json); @override@JsonKey() final int version; @@ -407,22 +998,11 @@ class _PersistedDocumentState extends PersistedDocumentState { @override final String? contentHash; @override final String? pageName; @override@JsonKey() final PersistedCameraState camera; -@override@JsonKey() final UtilitiesState utilities; +@override@JsonKey(readValue: _readLocks) final PersistentLockState locks; @override@JsonKey() final PersistedToolSelection selectedTool; -@override@JsonKey() final bool navigatorEnabled; -@override@JsonKey() final String navigatorPage; -@override@JsonKey() final String currentLayer; -@override@JsonKey() final String currentCollection; - final Set _invisibleLayers; -@override@JsonKey() Set get invisibleLayers { - if (_invisibleLayers is EqualUnmodifiableSetView) return _invisibleLayers; - // ignore: implicit_dynamic_type - return EqualUnmodifiableSetView(_invisibleLayers); -} - -@override@JsonKey() final bool areaNavigatorCreate; -@override@JsonKey() final bool areaNavigatorExact; -@override@JsonKey() final bool areaNavigatorAsk; +@override@JsonKey(readValue: _readNavigator) final PersistedNavigatorState navigator; +@override@JsonKey(readValue: _readLayers) final PersistedLayerState layers; +@override@JsonKey(readValue: _readAreaNavigator) final PersistedAreaNavigatorState areaNavigator; @override final DateTime? updatedAt; /// Create a copy of PersistedDocumentState @@ -438,16 +1018,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _PersistedDocumentState&&(identical(other.version, version) || other.version == version)&&(identical(other.pathKey, pathKey) || other.pathKey == pathKey)&&(identical(other.contentHash, contentHash) || other.contentHash == contentHash)&&(identical(other.pageName, pageName) || other.pageName == pageName)&&(identical(other.camera, camera) || other.camera == camera)&&(identical(other.utilities, utilities) || other.utilities == utilities)&&(identical(other.selectedTool, selectedTool) || other.selectedTool == selectedTool)&&(identical(other.navigatorEnabled, navigatorEnabled) || other.navigatorEnabled == navigatorEnabled)&&(identical(other.navigatorPage, navigatorPage) || other.navigatorPage == navigatorPage)&&(identical(other.currentLayer, currentLayer) || other.currentLayer == currentLayer)&&(identical(other.currentCollection, currentCollection) || other.currentCollection == currentCollection)&&const DeepCollectionEquality().equals(other._invisibleLayers, _invisibleLayers)&&(identical(other.areaNavigatorCreate, areaNavigatorCreate) || other.areaNavigatorCreate == areaNavigatorCreate)&&(identical(other.areaNavigatorExact, areaNavigatorExact) || other.areaNavigatorExact == areaNavigatorExact)&&(identical(other.areaNavigatorAsk, areaNavigatorAsk) || other.areaNavigatorAsk == areaNavigatorAsk)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _PersistedDocumentState&&(identical(other.version, version) || other.version == version)&&(identical(other.pathKey, pathKey) || other.pathKey == pathKey)&&(identical(other.contentHash, contentHash) || other.contentHash == contentHash)&&(identical(other.pageName, pageName) || other.pageName == pageName)&&(identical(other.camera, camera) || other.camera == camera)&&(identical(other.locks, locks) || other.locks == locks)&&(identical(other.selectedTool, selectedTool) || other.selectedTool == selectedTool)&&(identical(other.navigator, navigator) || other.navigator == navigator)&&(identical(other.layers, layers) || other.layers == layers)&&(identical(other.areaNavigator, areaNavigator) || other.areaNavigator == areaNavigator)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,version,pathKey,contentHash,pageName,camera,utilities,selectedTool,navigatorEnabled,navigatorPage,currentLayer,currentCollection,const DeepCollectionEquality().hash(_invisibleLayers),areaNavigatorCreate,areaNavigatorExact,areaNavigatorAsk,updatedAt); +int get hashCode => Object.hash(runtimeType,version,pathKey,contentHash,pageName,camera,locks,selectedTool,navigator,layers,areaNavigator,updatedAt); @override String toString() { - return 'PersistedDocumentState(version: $version, pathKey: $pathKey, contentHash: $contentHash, pageName: $pageName, camera: $camera, utilities: $utilities, selectedTool: $selectedTool, navigatorEnabled: $navigatorEnabled, navigatorPage: $navigatorPage, currentLayer: $currentLayer, currentCollection: $currentCollection, invisibleLayers: $invisibleLayers, areaNavigatorCreate: $areaNavigatorCreate, areaNavigatorExact: $areaNavigatorExact, areaNavigatorAsk: $areaNavigatorAsk, updatedAt: $updatedAt)'; + return 'PersistedDocumentState(version: $version, pathKey: $pathKey, contentHash: $contentHash, pageName: $pageName, camera: $camera, locks: $locks, selectedTool: $selectedTool, navigator: $navigator, layers: $layers, areaNavigator: $areaNavigator, updatedAt: $updatedAt)'; } @@ -458,11 +1038,11 @@ abstract mixin class _$PersistedDocumentStateCopyWith<$Res> implements $Persiste factory _$PersistedDocumentStateCopyWith(_PersistedDocumentState value, $Res Function(_PersistedDocumentState) _then) = __$PersistedDocumentStateCopyWithImpl; @override @useResult $Res call({ - int version, String? pathKey, String? contentHash, String? pageName, PersistedCameraState camera, UtilitiesState utilities, PersistedToolSelection selectedTool, bool navigatorEnabled, String navigatorPage, String currentLayer, String currentCollection, Set invisibleLayers, bool areaNavigatorCreate, bool areaNavigatorExact, bool areaNavigatorAsk, DateTime? updatedAt + int version, String? pathKey, String? contentHash, String? pageName, PersistedCameraState camera,@JsonKey(readValue: _readLocks) PersistentLockState locks, PersistedToolSelection selectedTool,@JsonKey(readValue: _readNavigator) PersistedNavigatorState navigator,@JsonKey(readValue: _readLayers) PersistedLayerState layers,@JsonKey(readValue: _readAreaNavigator) PersistedAreaNavigatorState areaNavigator, DateTime? updatedAt }); -@override $PersistedCameraStateCopyWith<$Res> get camera;@override $UtilitiesStateCopyWith<$Res> get utilities;@override $PersistedToolSelectionCopyWith<$Res> get selectedTool; +@override $PersistedCameraStateCopyWith<$Res> get camera;@override $PersistentLockStateCopyWith<$Res> get locks;@override $PersistedToolSelectionCopyWith<$Res> get selectedTool;@override $PersistedNavigatorStateCopyWith<$Res> get navigator;@override $PersistedLayerStateCopyWith<$Res> get layers;@override $PersistedAreaNavigatorStateCopyWith<$Res> get areaNavigator; } /// @nodoc @@ -475,24 +1055,19 @@ class __$PersistedDocumentStateCopyWithImpl<$Res> /// Create a copy of PersistedDocumentState /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? version = null,Object? pathKey = freezed,Object? contentHash = freezed,Object? pageName = freezed,Object? camera = null,Object? utilities = null,Object? selectedTool = null,Object? navigatorEnabled = null,Object? navigatorPage = null,Object? currentLayer = null,Object? currentCollection = null,Object? invisibleLayers = null,Object? areaNavigatorCreate = null,Object? areaNavigatorExact = null,Object? areaNavigatorAsk = null,Object? updatedAt = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? version = null,Object? pathKey = freezed,Object? contentHash = freezed,Object? pageName = freezed,Object? camera = null,Object? locks = null,Object? selectedTool = null,Object? navigator = null,Object? layers = null,Object? areaNavigator = null,Object? updatedAt = freezed,}) { return _then(_PersistedDocumentState( version: null == version ? _self.version : version // ignore: cast_nullable_to_non_nullable as int,pathKey: freezed == pathKey ? _self.pathKey : pathKey // ignore: cast_nullable_to_non_nullable as String?,contentHash: freezed == contentHash ? _self.contentHash : contentHash // ignore: cast_nullable_to_non_nullable as String?,pageName: freezed == pageName ? _self.pageName : pageName // ignore: cast_nullable_to_non_nullable as String?,camera: null == camera ? _self.camera : camera // ignore: cast_nullable_to_non_nullable -as PersistedCameraState,utilities: null == utilities ? _self.utilities : utilities // ignore: cast_nullable_to_non_nullable -as UtilitiesState,selectedTool: null == selectedTool ? _self.selectedTool : selectedTool // ignore: cast_nullable_to_non_nullable -as PersistedToolSelection,navigatorEnabled: null == navigatorEnabled ? _self.navigatorEnabled : navigatorEnabled // ignore: cast_nullable_to_non_nullable -as bool,navigatorPage: null == navigatorPage ? _self.navigatorPage : navigatorPage // ignore: cast_nullable_to_non_nullable -as String,currentLayer: null == currentLayer ? _self.currentLayer : currentLayer // ignore: cast_nullable_to_non_nullable -as String,currentCollection: null == currentCollection ? _self.currentCollection : currentCollection // ignore: cast_nullable_to_non_nullable -as String,invisibleLayers: null == invisibleLayers ? _self._invisibleLayers : invisibleLayers // ignore: cast_nullable_to_non_nullable -as Set,areaNavigatorCreate: null == areaNavigatorCreate ? _self.areaNavigatorCreate : areaNavigatorCreate // ignore: cast_nullable_to_non_nullable -as bool,areaNavigatorExact: null == areaNavigatorExact ? _self.areaNavigatorExact : areaNavigatorExact // ignore: cast_nullable_to_non_nullable -as bool,areaNavigatorAsk: null == areaNavigatorAsk ? _self.areaNavigatorAsk : areaNavigatorAsk // ignore: cast_nullable_to_non_nullable -as bool,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable +as PersistedCameraState,locks: null == locks ? _self.locks : locks // ignore: cast_nullable_to_non_nullable +as PersistentLockState,selectedTool: null == selectedTool ? _self.selectedTool : selectedTool // ignore: cast_nullable_to_non_nullable +as PersistedToolSelection,navigator: null == navigator ? _self.navigator : navigator // ignore: cast_nullable_to_non_nullable +as PersistedNavigatorState,layers: null == layers ? _self.layers : layers // ignore: cast_nullable_to_non_nullable +as PersistedLayerState,areaNavigator: null == areaNavigator ? _self.areaNavigator : areaNavigator // ignore: cast_nullable_to_non_nullable +as PersistedAreaNavigatorState,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime?, )); } @@ -510,10 +1085,10 @@ $PersistedCameraStateCopyWith<$Res> get camera { /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') -$UtilitiesStateCopyWith<$Res> get utilities { +$PersistentLockStateCopyWith<$Res> get locks { - return $UtilitiesStateCopyWith<$Res>(_self.utilities, (value) { - return _then(_self.copyWith(utilities: value)); + return $PersistentLockStateCopyWith<$Res>(_self.locks, (value) { + return _then(_self.copyWith(locks: value)); }); }/// Create a copy of PersistedDocumentState /// with the given fields replaced by the non-null parameter values. @@ -524,6 +1099,33 @@ $PersistedToolSelectionCopyWith<$Res> get selectedTool { return $PersistedToolSelectionCopyWith<$Res>(_self.selectedTool, (value) { return _then(_self.copyWith(selectedTool: value)); }); +}/// Create a copy of PersistedDocumentState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$PersistedNavigatorStateCopyWith<$Res> get navigator { + + return $PersistedNavigatorStateCopyWith<$Res>(_self.navigator, (value) { + return _then(_self.copyWith(navigator: value)); + }); +}/// Create a copy of PersistedDocumentState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$PersistedLayerStateCopyWith<$Res> get layers { + + return $PersistedLayerStateCopyWith<$Res>(_self.layers, (value) { + return _then(_self.copyWith(layers: value)); + }); +}/// Create a copy of PersistedDocumentState +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$PersistedAreaNavigatorStateCopyWith<$Res> get areaNavigator { + + return $PersistedAreaNavigatorStateCopyWith<$Res>(_self.areaNavigator, (value) { + return _then(_self.copyWith(areaNavigator: value)); + }); } } diff --git a/app/lib/models/persisted_document_state.g.dart b/app/lib/models/persisted_document_state.g.dart index ffe76bbe3c72..a0b8fe5daaa0 100644 --- a/app/lib/models/persisted_document_state.g.dart +++ b/app/lib/models/persisted_document_state.g.dart @@ -34,30 +34,37 @@ Map _$PersistedCameraStateToJson( 'zoom': instance.zoom, }; -_PersistedDocumentState _$PersistedDocumentStateFromJson(Map json) => - _PersistedDocumentState( - version: - (json['version'] as num?)?.toInt() ?? kPersistedDocumentStateVersion, - pathKey: json['pathKey'] as String?, - contentHash: json['contentHash'] as String?, - pageName: json['pageName'] as String?, - camera: json['camera'] == null - ? const PersistedCameraState() - : PersistedCameraState.fromJson( - Map.from(json['camera'] as Map), - ), - utilities: json['utilities'] == null - ? const UtilitiesState() - : UtilitiesState.fromJson( - Map.from(json['utilities'] as Map), - ), - selectedTool: json['selectedTool'] == null - ? const PersistedToolSelection() - : PersistedToolSelection.fromJson( - Map.from(json['selectedTool'] as Map), - ), - navigatorEnabled: json['navigatorEnabled'] as bool? ?? false, - navigatorPage: json['navigatorPage'] as String? ?? 'waypoints', +_PersistentLockState _$PersistentLockStateFromJson(Map json) => + _PersistentLockState( + lockCollection: json['lockCollection'] as bool? ?? false, + lockLayer: json['lockLayer'] as bool? ?? false, + lockZoom: json['lockZoom'] as bool? ?? false, + lockHorizontal: json['lockHorizontal'] as bool? ?? false, + lockVertical: json['lockVertical'] as bool? ?? false, + ); + +Map _$PersistentLockStateToJson( + _PersistentLockState instance, +) => { + 'lockCollection': instance.lockCollection, + 'lockLayer': instance.lockLayer, + 'lockZoom': instance.lockZoom, + 'lockHorizontal': instance.lockHorizontal, + 'lockVertical': instance.lockVertical, +}; + +_PersistedNavigatorState _$PersistedNavigatorStateFromJson(Map json) => + _PersistedNavigatorState( + enabled: json['enabled'] as bool? ?? false, + page: json['page'] as String? ?? 'waypoints', + ); + +Map _$PersistedNavigatorStateToJson( + _PersistedNavigatorState instance, +) => {'enabled': instance.enabled, 'page': instance.page}; + +_PersistedLayerState _$PersistedLayerStateFromJson(Map json) => + _PersistedLayerState( currentLayer: json['currentLayer'] as String? ?? '', currentCollection: json['currentCollection'] as String? ?? '', invisibleLayers: @@ -65,14 +72,75 @@ _PersistedDocumentState _$PersistedDocumentStateFromJson(Map json) => ?.map((e) => e as String) .toSet() ?? const {}, - areaNavigatorCreate: json['areaNavigatorCreate'] as bool? ?? true, - areaNavigatorExact: json['areaNavigatorExact'] as bool? ?? true, - areaNavigatorAsk: json['areaNavigatorAsk'] as bool? ?? false, - updatedAt: json['updatedAt'] == null - ? null - : DateTime.parse(json['updatedAt'] as String), ); +Map _$PersistedLayerStateToJson( + _PersistedLayerState instance, +) => { + 'currentLayer': instance.currentLayer, + 'currentCollection': instance.currentCollection, + 'invisibleLayers': instance.invisibleLayers.toList(), +}; + +_PersistedAreaNavigatorState _$PersistedAreaNavigatorStateFromJson(Map json) => + _PersistedAreaNavigatorState( + create: json['create'] as bool? ?? true, + exact: json['exact'] as bool? ?? true, + ask: json['ask'] as bool? ?? false, + ); + +Map _$PersistedAreaNavigatorStateToJson( + _PersistedAreaNavigatorState instance, +) => { + 'create': instance.create, + 'exact': instance.exact, + 'ask': instance.ask, +}; + +_PersistedDocumentState _$PersistedDocumentStateFromJson( + Map json, +) => _PersistedDocumentState( + version: (json['version'] as num?)?.toInt() ?? kPersistedDocumentStateVersion, + pathKey: json['pathKey'] as String?, + contentHash: json['contentHash'] as String?, + pageName: json['pageName'] as String?, + camera: json['camera'] == null + ? const PersistedCameraState() + : PersistedCameraState.fromJson( + Map.from(json['camera'] as Map), + ), + locks: _readLocks(json, 'locks') == null + ? const PersistentLockState() + : PersistentLockState.fromJson( + Map.from(_readLocks(json, 'locks') as Map), + ), + selectedTool: json['selectedTool'] == null + ? const PersistedToolSelection() + : PersistedToolSelection.fromJson( + Map.from(json['selectedTool'] as Map), + ), + navigator: _readNavigator(json, 'navigator') == null + ? const PersistedNavigatorState() + : PersistedNavigatorState.fromJson( + Map.from(_readNavigator(json, 'navigator') as Map), + ), + layers: _readLayers(json, 'layers') == null + ? const PersistedLayerState() + : PersistedLayerState.fromJson( + Map.from(_readLayers(json, 'layers') as Map), + ), + areaNavigator: _readAreaNavigator(json, 'areaNavigator') == null + ? const PersistedAreaNavigatorState() + : PersistedAreaNavigatorState.fromJson( + Map.from( + _readAreaNavigator(json, 'areaNavigator') as Map, + ), + ), + updatedAt: json['updatedAt'] == null + ? null + : DateTime.parse(json['updatedAt'] as String), +); + Map _$PersistedDocumentStateToJson( _PersistedDocumentState instance, ) => { @@ -81,15 +149,10 @@ Map _$PersistedDocumentStateToJson( 'contentHash': instance.contentHash, 'pageName': instance.pageName, 'camera': instance.camera.toJson(), - 'utilities': instance.utilities.toJson(), + 'locks': instance.locks.toJson(), 'selectedTool': instance.selectedTool.toJson(), - 'navigatorEnabled': instance.navigatorEnabled, - 'navigatorPage': instance.navigatorPage, - 'currentLayer': instance.currentLayer, - 'currentCollection': instance.currentCollection, - 'invisibleLayers': instance.invisibleLayers.toList(), - 'areaNavigatorCreate': instance.areaNavigatorCreate, - 'areaNavigatorExact': instance.areaNavigatorExact, - 'areaNavigatorAsk': instance.areaNavigatorAsk, + 'navigator': instance.navigator.toJson(), + 'layers': instance.layers.toJson(), + 'areaNavigator': instance.areaNavigator.toJson(), 'updatedAt': instance.updatedAt?.toIso8601String(), }; diff --git a/app/lib/repositories/document_state.dart b/app/lib/repositories/document_state.dart new file mode 100644 index 000000000000..dc291856a3fd --- /dev/null +++ b/app/lib/repositories/document_state.dart @@ -0,0 +1,48 @@ +import 'package:butterfly/api/file_system.dart'; +import 'package:butterfly/models/persisted_document_state.dart'; + +class DocumentStateRepository { + DocumentStateRepository(this.fileSystem); + + final DocumentStateFileSystem fileSystem; + + Future load({ + String? contentHash, + String? pathKey, + bool allowContentHash = true, + }) async { + await fileSystem.initialize(); + if (allowContentHash && contentHash != null) { + final byContent = await fileSystem.getFile( + documentStateContentKey(contentHash), + ); + if (byContent != null) return byContent; + } + if (pathKey != null) { + return fileSystem.getFile(pathKey); + } + return null; + } + + Future save( + PersistedDocumentState state, { + String? contentHash, + String? pathKey, + }) async { + await fileSystem.initialize(); + if (contentHash != null) { + await _put(documentStateContentKey(contentHash), state); + } + if (pathKey != null) { + await _put(pathKey, state); + } + } + + Future _put(String key, PersistedDocumentState state) async { + if (await fileSystem.hasKey(key)) { + await fileSystem.updateFile(key, state); + } else { + await fileSystem.createFile(key, state); + } + } +} diff --git a/app/lib/selections/document.dart b/app/lib/selections/document.dart index a30b44ccb852..6ef876aefb34 100644 --- a/app/lib/selections/document.dart +++ b/app/lib/selections/document.dart @@ -20,29 +20,18 @@ class DocumentSelection extends Selection { return [ ...super.buildProperties(context), _UtilitiesView( - state: viewState.utilities, - option: viewState.viewOption, - onStateChanged: (state) => - cubit.viewCubit.updateUtilities(utilities: state), - onToolChanged: (option) => - cubit.viewCubit.updateUtilities(view: option), + state: viewState.locks, + onStateChanged: (state) => cubit.viewCubit.updateLocks(locks: state), ), ]; } } class _UtilitiesView extends StatefulWidget { - final UtilitiesState state; - final ViewOption option; - final ValueChanged onStateChanged; - final ValueChanged onToolChanged; + final PersistentLockState state; + final ValueChanged onStateChanged; - const _UtilitiesView({ - required this.state, - required this.option, - required this.onStateChanged, - required this.onToolChanged, - }); + const _UtilitiesView({required this.state, required this.onStateChanged}); @override State<_UtilitiesView> createState() => _UtilitiesViewState(); diff --git a/app/lib/selections/selection.dart b/app/lib/selections/selection.dart index e66f979d0b58..83ae063dce20 100644 --- a/app/lib/selections/selection.dart +++ b/app/lib/selections/selection.dart @@ -7,6 +7,7 @@ import 'package:butterfly/dialogs/constraints.dart'; import 'package:butterfly/dialogs/texture.dart'; import 'package:butterfly/dialogs/export/thumbnail.dart'; import 'package:butterfly/helpers/point.dart'; +import 'package:butterfly/models/persisted_document_state.dart'; import 'package:butterfly/renderers/renderer.dart'; import 'package:butterfly/visualizer/tool.dart'; import 'package:butterfly/visualizer/preset.dart'; diff --git a/app/lib/views/app_bar.dart b/app/lib/views/app_bar.dart index 25fe41038148..605286859ac8 100644 --- a/app/lib/views/app_bar.dart +++ b/app/lib/views/app_bar.dart @@ -34,7 +34,9 @@ import '../actions/save.dart'; import '../bloc/document_bloc.dart'; import '../cubits/settings.dart'; import '../embed/action.dart'; +import '../embed/embedding.dart'; import 'navigator/view.dart'; +import 'package:lw_file_system/lw_file_system.dart'; class PadAppBar extends StatelessWidget implements PreferredSizeWidget { final GlobalKey viewportKey; @@ -491,24 +493,35 @@ class MainPopupMenu extends StatelessWidget { Widget build(BuildContext context) { final cubit = context.read(); final windowCubit = context.read(); - return BlocBuilder( - buildWhen: (previous, current) => - previous.navigationRail != current.navigationRail || - previous.flags != current.flags, + return BlocSelector< + SettingsCubit, + ButterflySettings, + ({bool collaboration, List history, bool navigationRail}) + >( + selector: (state) => ( + collaboration: state.hasFlag('collaboration'), + history: state.history, + navigationRail: state.navigationRail, + ), builder: (context, settings) { - return BlocBuilder( - buildWhen: (previous, current) => - previous.fullScreen != current.fullScreen, - builder: (context, windowState) { - return BlocBuilder( - buildWhen: (previous, current) => - previous.embedding != current.embedding || - previous.saved != current.saved, + return BlocSelector( + selector: (state) => state.fullScreen, + builder: (context, fullScreen) { + return BlocSelector< + DocumentSaveCubit, + DocumentSaveState, + ({Embedding? embedding, SaveState saved}) + >( + selector: (state) => + (embedding: state.embedding, saved: state.saved), builder: (context, saveState) { - return BlocBuilder( - buildWhen: (previous, current) => - previous.hideUi != current.hideUi, - builder: (context, inputState) { + return BlocSelector< + EditorInputCubit, + EditorInputState, + HideState + >( + selector: (state) => state.hideUi, + builder: (context, hideUi) { final size = MediaQuery.sizeOf(context); final navigatorRailEnabled = settings.navigationRail || saveState.embedding != null; @@ -516,8 +529,8 @@ class MainPopupMenu extends StatelessWidget { MediaQuery.sizeOf(context).width < LeapBreakpoints.expanded || !navigatorRailEnabled || - windowState.fullScreen || - inputState.hideUi != HideState.visible; + fullScreen || + hideUi != HideState.visible; return MenuAnchor( menuChildren: [ if (showNavigatorDialog) @@ -824,7 +837,7 @@ class MainPopupMenu extends StatelessWidget { ), ], if (saveState.embedding == null && - settings.hasFlag('collaboration')) + settings.collaboration) BlocBuilder( bloc: context .read() diff --git a/app/lib/views/edit.dart b/app/lib/views/edit.dart index bdbbce54f4ca..b3de5aeb319d 100644 --- a/app/lib/views/edit.dart +++ b/app/lib/views/edit.dart @@ -1,6 +1,7 @@ import 'package:butterfly/bloc/document_bloc.dart'; import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/dialogs/import/add.dart'; +import 'package:butterfly/models/persisted_document_state.dart'; import 'package:butterfly/services/import.dart'; import 'package:butterfly/visualizer/tool.dart'; import 'package:butterfly/widgets/option_button.dart'; @@ -445,10 +446,10 @@ class _EditToolbarState extends State { ), BlocBuilder( builder: (context, viewState) { - final utilitiesState = viewState.utilities; + final locks = viewState.locks; Widget buildButton( bool selected, - UtilitiesState Function() update, + PersistentLockState Function() update, PhosphorIconData icon, String title, ) => CheckboxMenuButton( @@ -456,50 +457,45 @@ class _EditToolbarState extends State { trailingIcon: PhosphorIcon(icon), onChanged: (value) => context .read() - .updateUtilities(utilities: update()), + .updateLocks(locks: update()), child: Text(title), ); return MenuAnchor( menuChildren: [ buildButton( - utilitiesState.lockCollection, - () => utilitiesState.copyWith( - lockCollection: - !utilitiesState.lockCollection, + locks.lockCollection, + () => locks.copyWith( + lockCollection: !locks.lockCollection, ), PhosphorIconsLight.folder, AppLocalizations.of(context).collection, ), buildButton( - utilitiesState.lockLayer, - () => utilitiesState.copyWith( - lockLayer: !utilitiesState.lockLayer, - ), + locks.lockLayer, + () => + locks.copyWith(lockLayer: !locks.lockLayer), PhosphorIconsLight.folder, AppLocalizations.of(context).layer, ), buildButton( - utilitiesState.lockZoom, - () => utilitiesState.copyWith( - lockZoom: !utilitiesState.lockZoom, - ), + locks.lockZoom, + () => locks.copyWith(lockZoom: !locks.lockZoom), PhosphorIconsLight.magnifyingGlassPlus, AppLocalizations.of(context).zoom, ), buildButton( - utilitiesState.lockHorizontal, - () => utilitiesState.copyWith( - lockHorizontal: - !utilitiesState.lockHorizontal, + locks.lockHorizontal, + () => locks.copyWith( + lockHorizontal: !locks.lockHorizontal, ), PhosphorIconsLight.arrowsHorizontal, AppLocalizations.of(context).horizontal, ), buildButton( - utilitiesState.lockVertical, - () => utilitiesState.copyWith( - lockVertical: !utilitiesState.lockVertical, + locks.lockVertical, + () => locks.copyWith( + lockVertical: !locks.lockVertical, ), PhosphorIconsLight.arrowsVertical, AppLocalizations.of(context).vertical, diff --git a/app/lib/views/main.dart b/app/lib/views/main.dart index b261bdd19c30..a176a9539d2c 100644 --- a/app/lib/views/main.dart +++ b/app/lib/views/main.dart @@ -13,6 +13,7 @@ import 'package:butterfly/embed/embedding.dart'; import 'package:butterfly/models/defaults.dart'; import 'package:butterfly/models/persisted_document_state.dart'; import 'package:butterfly/renderers/renderer.dart'; +import 'package:butterfly/repositories/document_state.dart'; import 'package:butterfly/services/export.dart'; import 'package:butterfly/services/import.dart'; import 'package:butterfly/services/network.dart'; @@ -340,9 +341,10 @@ class _ProjectPageState extends State { final contentHash = loadedDocumentBytes == null ? null : documentStateContentHash(loadedDocumentBytes); - final documentStateSystem = fileSystem.buildDocumentStateSystem(remote); - final restoredSession = await EditorSessionCubit.load( - fileSystem: documentStateSystem, + final documentStateRepository = DocumentStateRepository( + fileSystem.buildDocumentStateSystem(remote), + ); + final restoredSession = await documentStateRepository.load( contentHash: contentHash, pathKey: pathKey, allowContentHash: loadedDocumentBytes != null, @@ -361,7 +363,7 @@ class _ProjectPageState extends State { document: document, page: page, fallbackPageName: pageName, - fallbackUtilities: settingsCubit.state.utilities, + fallbackLocks: const PersistentLockState(), pathKey: pathKey, contentHash: contentHash, ); @@ -382,7 +384,7 @@ class _ProjectPageState extends State { initialSession.camera.zoom, ); final editorSessionCubit = EditorSessionCubit( - fileSystem: documentStateSystem, + repository: documentStateRepository, transformCubit: transformCubit, initialState: initialSession, pathKey: pathKey, @@ -439,11 +441,11 @@ class _ProjectPageState extends State { page, pageName, false, - initialSession.currentLayer.isEmpty + initialSession.layers.currentLayer.isEmpty ? null - : initialSession.currentLayer, - initialSession.currentCollection, - initialSession.invisibleLayers, + : initialSession.layers.currentLayer, + initialSession.layers.currentCollection, + initialSession.layers.invisibleLayers, ); final isImportedDocument = documentOpened && !(location.fileType?.isNote() ?? false); @@ -559,24 +561,31 @@ class _ProjectPageState extends State { FocusManager.instance.primaryFocus?.unfocus(); } }, - child: BlocBuilder( - builder: (context, windowState) => - BlocBuilder( - buildWhen: (previous, current) => - previous.hideUi != current.hideUi, - builder: (context, inputState) => - BlocBuilder( - buildWhen: (previous, current) => - previous.embedding?.editable != - current.embedding?.editable || - previous.embedding?.isInternal != - current.embedding?.isInternal, + child: BlocSelector( + selector: (state) => state.fullScreen, + builder: (context, fullScreen) => + BlocSelector( + selector: (state) => state.hideUi, + builder: (context, hideUi) => + BlocSelector< + DocumentSaveCubit, + DocumentSaveState, + ({bool editable, bool inView}) + >( + selector: (state) => ( + editable: state.embedding?.editable != false, + inView: state.embedding?.isInternal ?? false, + ), builder: (context, saveState) => - BlocBuilder( - buildWhen: (previous, current) => - previous.toolbarSize != - current.toolbarSize || - previous.isInline != current.isInline, + BlocSelector< + SettingsCubit, + ButterflySettings, + ({bool isInline, ToolbarSize toolbarSize}) + >( + selector: (state) => ( + isInline: state.isInline, + toolbarSize: state.toolbarSize, + ), builder: (context, settings) { final actions = _buildActions(context); @@ -600,9 +609,8 @@ class _ProjectPageState extends State { child: Scaffold( appBar: state is DocumentPresentationState || - windowState.fullScreen || - inputState.hideUi != - HideState.visible + fullScreen || + hideUi != HideState.visible ? null : PadAppBar( viewportKey: _viewportKey, @@ -614,17 +622,10 @@ class _ProjectPageState extends State { Directionality.of( context, ), - inView: - saveState - .embedding - ?.isInternal ?? - false, + inView: saveState.inView, showTools: settings.isInline && - saveState - .embedding - ?.editable != - false, + saveState.editable, ), body: const _MainBody(), ), diff --git a/app/lib/views/pen_only_toggle.dart b/app/lib/views/pen_only_toggle.dart index 788067d113b3..d57d75608a2a 100644 --- a/app/lib/views/pen_only_toggle.dart +++ b/app/lib/views/pen_only_toggle.dart @@ -13,20 +13,29 @@ class PenOnlyToggle extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => - previous.runtimeType != current.runtimeType, - builder: (context, docState) => - BlocBuilder( - buildWhen: (previous, current) => - previous.penDetected != current.penDetected || - previous.hideUi != current.hideUi || - previous.sessionPenOnlyInput != current.sessionPenOnlyInput, + return BlocSelector( + selector: (state) => state is DocumentLoadSuccess, + builder: (context, loaded) => + BlocSelector< + EditorInputCubit, + EditorInputState, + ({HideState hideUi, bool penDetected, bool? sessionPenOnlyInput}) + >( + selector: (state) => ( + hideUi: state.hideUi, + penDetected: state.penDetected, + sessionPenOnlyInput: state.sessionPenOnlyInput, + ), builder: (context, inputState) => - BlocBuilder( - buildWhen: (previous, current) => - previous.penOnlyInput != current.penOnlyInput || - previous.showPenOnlyToggle != current.showPenOnlyToggle, + BlocSelector< + SettingsCubit, + ButterflySettings, + ({bool? penOnlyInput, bool showPenOnlyToggle}) + >( + selector: (state) => ( + penOnlyInput: state.penOnlyInput, + showPenOnlyToggle: state.showPenOnlyToggle, + ), builder: (context, settings) { // Don't show if: // - No pen has been detected @@ -36,7 +45,7 @@ class PenOnlyToggle extends StatelessWidget { if (!inputState.penDetected || inputState.hideUi != HideState.visible || !settings.showPenOnlyToggle || - docState is! DocumentLoadSuccess) { + !loaded) { return const SizedBox.shrink(); } diff --git a/app/lib/views/zoom.dart b/app/lib/views/zoom.dart index ffbf109eb720..c80e515b5a8c 100644 --- a/app/lib/views/zoom.dart +++ b/app/lib/views/zoom.dart @@ -97,129 +97,131 @@ class _ZoomViewState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => - previous.runtimeType != current.runtimeType, - builder: (context, state) => BlocBuilder( - builder: (context, windowState) => - BlocBuilder( - buildWhen: (previous, current) => - previous.zoomEnabled != current.zoomEnabled, - builder: (context, settings) => - BlocBuilder( - buildWhen: (previous, current) => - previous.size != current.size, - builder: (context, transform) { - var scale = transform.size; - final editorController = context.read(); - final hideZoom = - !settings.zoomEnabled || - windowState.fullScreen || - editorController.inputCubit.state.hideUi != - HideState.visible; + return BlocSelector( + selector: (state) => state is DocumentLoadSuccess, + builder: (context, loaded) => + BlocSelector( + selector: (state) => state.fullScreen, + builder: (context, fullScreen) => + BlocSelector( + selector: (state) => state.zoomEnabled, + builder: (context, zoomEnabled) => + BlocSelector( + selector: (state) => state.size, + builder: (context, transformSize) { + var scale = transformSize; + final editorController = context + .read(); + final hideZoom = + !zoomEnabled || + fullScreen || + editorController.inputCubit.state.hideUi != + HideState.visible; - final body = StatefulBuilder( - builder: (context, setState) { - final text = (scale * 100).toStringAsFixed(0); - if (text != _zoomController.text) { - _zoomController.text = text; - } - return LayoutBuilder( - builder: (context, constraints) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: 75, - child: TextFormField( - textAlign: TextAlign.center, - controller: _zoomController, - keyboardType: TextInputType.number, - focusNode: _focusNode, - onChanged: (value) { - setState( - () => scale = - (parseDoubleInput(value) ?? - (scale * 100)) / - 100, - ); - }, - onEditingComplete: () => _zoom(scale), - onTapOutside: (event) { - if (!_focusNode.hasFocus) return; - _zoom(scale); - _focusNode.unfocus(); - }, - onFieldSubmitted: (value) => _zoom(scale), - ), - ), - const SizedBox(width: 8), - Tooltip( - message: AppLocalizations.of( - context, - ).resetZoom, - child: IconButton( - icon: const Icon( - PhosphorIconsLight - .clockCounterClockwise, + final body = StatefulBuilder( + builder: (context, setState) { + final text = (scale * 100).toStringAsFixed(0); + if (text != _zoomController.text) { + _zoomController.text = text; + } + return LayoutBuilder( + builder: (context, constraints) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 75, + child: TextFormField( + textAlign: TextAlign.center, + controller: _zoomController, + keyboardType: TextInputType.number, + focusNode: _focusNode, + onChanged: (value) { + setState( + () => scale = + (parseDoubleInput(value) ?? + (scale * 100)) / + 100, + ); + }, + onEditingComplete: () => _zoom(scale), + onTapOutside: (event) { + if (!_focusNode.hasFocus) return; + _zoom(scale); + _focusNode.unfocus(); + }, + onFieldSubmitted: (value) => + _zoom(scale), + ), ), - onPressed: () { - _zoom(1.0); - }, - ), - ), - if (!widget.isMobile) ...[ - if (constraints.maxWidth > 200) - Flexible( - child: Slider( - value: scale.clamp(kMinZoom, 10), - min: kMinZoom, - max: 10, - onChanged: (value) => - _zoom(value, false), - onChangeEnd: _zoom, + const SizedBox(width: 8), + Tooltip( + message: AppLocalizations.of( + context, + ).resetZoom, + child: IconButton( + icon: const Icon( + PhosphorIconsLight + .clockCounterClockwise, + ), + onPressed: () { + _zoom(1.0); + }, ), ), - ], - ], + if (!widget.isMobile) ...[ + if (constraints.maxWidth > 200) + Flexible( + child: Slider( + value: scale.clamp(kMinZoom, 10), + min: kMinZoom, + max: 10, + onChanged: (value) => + _zoom(value, false), + onChangeEnd: _zoom, + ), + ), + ], + ], + ); + }, ); }, ); - }, - ); - if ((!_focusNode.hasFocus && widget.isMobile) || - hideZoom) { - _controller.reverse(); - } else { - _controller.forward(); - } - return AnimatedBuilder( - animation: _animation, - child: body, - builder: (context, child) { - if (_animation.value == 0 || - state is! DocumentLoadSuccess) { - return const SizedBox(); + if ((!_focusNode.hasFocus && widget.isMobile) || + hideZoom) { + _controller.reverse(); + } else { + _controller.forward(); } - return ConstrainedBox( - constraints: const BoxConstraints(minWidth: 400), - child: Opacity( - opacity: _animation.value, - child: Card( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: child, + return AnimatedBuilder( + animation: _animation, + child: body, + builder: (context, child) { + if (_animation.value == 0 || !loaded) { + return const SizedBox(); + } + return ConstrainedBox( + constraints: const BoxConstraints( + minWidth: 400, + ), + child: Opacity( + opacity: _animation.value, + child: Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: child, + ), + ), ), - ), - ), + ); + }, ); }, - ); - }, - ), - ), - ), + ), + ), + ), ); } } diff --git a/app/test/cubits/editor_session_test.dart b/app/test/cubits/editor_session_test.dart index 7f3029e00be0..e0194a29f721 100644 --- a/app/test/cubits/editor_session_test.dart +++ b/app/test/cubits/editor_session_test.dart @@ -2,6 +2,7 @@ import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/cubits/editor_session.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/models/persisted_document_state.dart'; +import 'package:butterfly/repositories/document_state.dart'; import 'package:butterfly/views/navigator/view.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter/foundation.dart'; @@ -22,17 +23,21 @@ void main() { positionY: 20, zoom: 2, ), - utilities: const UtilitiesState(lockZoom: true), + locks: const PersistentLockState(lockZoom: true), selectedTool: const PersistedToolSelection( toolId: 'tool-a', toolIndex: 3, ), - navigatorEnabled: true, - navigatorPage: NavigatorPage.layers.name, - currentLayer: 'layer-a', - currentCollection: 'collection-a', - invisibleLayers: {'hidden-a'}, - areaNavigatorCreate: false, + navigator: PersistedNavigatorState( + enabled: true, + page: NavigatorPage.layers.name, + ), + layers: const PersistedLayerState( + currentLayer: 'layer-a', + currentCollection: 'collection-a', + invisibleLayers: {'hidden-a'}, + ), + areaNavigator: const PersistedAreaNavigatorState(create: false), updatedAt: DateTime.utc(2026), ); @@ -48,8 +53,8 @@ void main() { expect(state.version, kPersistedDocumentStateVersion); expect(state.camera.zoom, 1); - expect(state.utilities, const UtilitiesState()); - expect(state.navigatorPage, NavigatorPage.waypoints.name); + expect(state.locks, const PersistentLockState()); + expect(state.navigator.page, NavigatorPage.waypoints.name); }); }); @@ -67,11 +72,9 @@ void main() { await fileSystem.createFile(documentStateContentKey('hash-a'), byContent); await fileSystem.createFile('path/a', byPath); - final loaded = await EditorSessionCubit.load( - fileSystem: fileSystem, - contentHash: 'hash-a', - pathKey: 'path/a', - ); + final loaded = await DocumentStateRepository( + fileSystem, + ).load(contentHash: 'hash-a', pathKey: 'path/a'); expect(loaded?.pageName, 'Content Page'); }); @@ -81,21 +84,17 @@ void main() { await fileSystem.initialize(); await fileSystem.createFile('path/a', byPath); - final loaded = await EditorSessionCubit.load( - fileSystem: fileSystem, - contentHash: 'missing', - pathKey: 'path/a', - ); + final loaded = await DocumentStateRepository( + fileSystem, + ).load(contentHash: 'missing', pathKey: 'path/a'); expect(loaded?.pageName, 'Path Page'); }); test('returns null when no fingerprints match', () async { - final loaded = await EditorSessionCubit.load( - fileSystem: fileSystem, - contentHash: 'missing', - pathKey: 'path/missing', - ); + final loaded = await DocumentStateRepository( + fileSystem, + ).load(contentHash: 'missing', pathKey: 'path/missing'); expect(loaded, isNull); }); @@ -103,7 +102,7 @@ void main() { test('writes session state to content and path keys', () async { final transformCubit = TransformCubit(1); final cubit = EditorSessionCubit( - fileSystem: fileSystem, + repository: DocumentStateRepository(fileSystem), transformCubit: transformCubit, initialState: const PersistedDocumentState(pageName: 'Page 1'), pathKey: 'path/a', @@ -116,7 +115,7 @@ void main() { expect( (await fileSystem.getFile( documentStateContentKey('hash-a'), - ))?.navigatorPage, + ))?.navigator.page, NavigatorPage.layers.name, ); final contentRecord = await fileSystem.getFile( @@ -127,7 +126,7 @@ void main() { expect(contentRecord?.contentHash, 'hash-a'); expect(pathRecord?.pathKey, 'path/a'); expect(pathRecord?.contentHash, 'hash-a'); - expect(pathRecord?.navigatorEnabled, isTrue); + expect(pathRecord?.navigator.enabled, isTrue); await cubit.close(); await transformCubit.close(); @@ -141,7 +140,7 @@ void main() { final transformCubit = TransformCubit(1); final cubit = EditorSessionCubit( - fileSystem: fileSystem, + repository: DocumentStateRepository(fileSystem), transformCubit: transformCubit, initialState: const PersistedDocumentState(pageName: 'Page 1'), pathKey: 'path/test', From 4c5d0240b75cd62d8407826236262edf98a98eb3 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 30 Jun 2026 20:03:33 +0200 Subject: [PATCH 037/117] Add persistence settings --- app/lib/cubits/settings.dart | 41 ++++ app/lib/cubits/settings.freezed.dart | 230 ++++++++++++++++-- app/lib/cubits/settings.g.dart | 36 +++ app/lib/l10n/app_en.arb | 3 +- app/lib/main.dart | 10 +- app/lib/repositories/document_state.dart | 137 ++++++++++- .../{behaviors.dart => behaviors/home.dart} | 18 ++ app/lib/settings/behaviors/persistence.dart | 130 ++++++++++ app/lib/settings/home.dart | 2 +- app/lib/views/main.dart | 1 + app/test/cubits/editor_session_test.dart | 99 ++++++++ 11 files changed, 677 insertions(+), 30 deletions(-) rename app/lib/settings/{behaviors.dart => behaviors/home.dart} (94%) create mode 100644 app/lib/settings/behaviors/persistence.dart diff --git a/app/lib/cubits/settings.dart b/app/lib/cubits/settings.dart index 9d113a73956a..f23d336e560c 100644 --- a/app/lib/cubits/settings.dart +++ b/app/lib/cubits/settings.dart @@ -436,6 +436,27 @@ class SRGBConverter extends JsonConverter { enum SaveMethod { manual, autosave, delayedAutosave } +@freezed +sealed class DocumentStatePersistenceSettings + with _$DocumentStatePersistenceSettings { + const factory DocumentStatePersistenceSettings({ + @Default(true) bool enabled, + @Default(true) bool page, + @Default(true) bool camera, + @Default(true) bool locks, + @Default(true) bool tool, + @Default(true) bool navigator, + @Default(true) bool layers, + @Default(true) bool areas, + @Default(400) int maxEntries, + @Default(180) int maxAgeDays, + }) = _DocumentStatePersistenceSettings; + + factory DocumentStatePersistenceSettings.fromJson( + Map json, + ) => _$DocumentStatePersistenceSettingsFromJson(json); +} + @freezed sealed class ButterflySettings with _$ButterflySettings, LeapSettings { const ButterflySettings._(); @@ -496,6 +517,8 @@ sealed class ButterflySettings with _$ButterflySettings, LeapSettings { @Default(3) int autosaveDelaySeconds, @Default(false) bool hideCursorWhileDrawing, @Default(StartupBehavior.openHomeScreen) StartupBehavior onStartup, + @Default(DocumentStatePersistenceSettings()) + DocumentStatePersistenceSettings documentStatePersistence, @Default(SimpleToolbarVisibility.show) SimpleToolbarVisibility simpleToolbarVisibility, @Default(OptionsPanelPosition.top) @@ -685,6 +708,13 @@ sealed class ButterflySettings with _$ButterflySettings, LeapSettings { StartupBehavior.openHomeScreen, ) : StartupBehavior.openHomeScreen, + documentStatePersistence: prefs.containsKey('document_state_persistence') + ? DocumentStatePersistenceSettings.fromJson( + _decodeJsonMapOrEmpty( + prefs.getString('document_state_persistence'), + ), + ) + : const DocumentStatePersistenceSettings(), simpleToolbarVisibility: prefs.containsKey('simple_toolbar_visibility') ? _enumByNameOr( SimpleToolbarVisibility.values, @@ -839,6 +869,10 @@ sealed class ButterflySettings with _$ButterflySettings, LeapSettings { await prefs.setBool('hide_cursor_while_drawing', hideCursorWhileDrawing); await prefs.setString('navigator_position', navigatorPosition.name); await prefs.setString('on_startup', onStartup.name); + await prefs.setString( + 'document_state_persistence', + json.encode(documentStatePersistence.toJson()), + ); await prefs.setString( 'simple_toolbar_visibility', simpleToolbarVisibility.name, @@ -1511,6 +1545,13 @@ class SettingsCubit extends Cubit return save(); } + Future changeDocumentStatePersistence( + DocumentStatePersistenceSettings settings, + ) { + emit(state.copyWith(documentStatePersistence: settings)); + return save(); + } + Future changeSimpleToolbarVisibility( SimpleToolbarVisibility visibility, ) { diff --git a/app/lib/cubits/settings.freezed.dart b/app/lib/cubits/settings.freezed.dart index f91c69a59745..c682f02266fd 100644 --- a/app/lib/cubits/settings.freezed.dart +++ b/app/lib/cubits/settings.freezed.dart @@ -534,10 +534,183 @@ as String?, } +/// @nodoc +mixin _$DocumentStatePersistenceSettings implements DiagnosticableTreeMixin { + + bool get enabled; bool get page; bool get camera; bool get locks; bool get tool; bool get navigator; bool get layers; bool get areas; int get maxEntries; int get maxAgeDays; +/// Create a copy of DocumentStatePersistenceSettings +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$DocumentStatePersistenceSettingsCopyWith get copyWith => _$DocumentStatePersistenceSettingsCopyWithImpl(this as DocumentStatePersistenceSettings, _$identity); + + /// Serializes this DocumentStatePersistenceSettings to a JSON map. + Map toJson(); + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'DocumentStatePersistenceSettings')) + ..add(DiagnosticsProperty('enabled', enabled))..add(DiagnosticsProperty('page', page))..add(DiagnosticsProperty('camera', camera))..add(DiagnosticsProperty('locks', locks))..add(DiagnosticsProperty('tool', tool))..add(DiagnosticsProperty('navigator', navigator))..add(DiagnosticsProperty('layers', layers))..add(DiagnosticsProperty('areas', areas))..add(DiagnosticsProperty('maxEntries', maxEntries))..add(DiagnosticsProperty('maxAgeDays', maxAgeDays)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is DocumentStatePersistenceSettings&&(identical(other.enabled, enabled) || other.enabled == enabled)&&(identical(other.page, page) || other.page == page)&&(identical(other.camera, camera) || other.camera == camera)&&(identical(other.locks, locks) || other.locks == locks)&&(identical(other.tool, tool) || other.tool == tool)&&(identical(other.navigator, navigator) || other.navigator == navigator)&&(identical(other.layers, layers) || other.layers == layers)&&(identical(other.areas, areas) || other.areas == areas)&&(identical(other.maxEntries, maxEntries) || other.maxEntries == maxEntries)&&(identical(other.maxAgeDays, maxAgeDays) || other.maxAgeDays == maxAgeDays)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,enabled,page,camera,locks,tool,navigator,layers,areas,maxEntries,maxAgeDays); + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'DocumentStatePersistenceSettings(enabled: $enabled, page: $page, camera: $camera, locks: $locks, tool: $tool, navigator: $navigator, layers: $layers, areas: $areas, maxEntries: $maxEntries, maxAgeDays: $maxAgeDays)'; +} + + +} + +/// @nodoc +abstract mixin class $DocumentStatePersistenceSettingsCopyWith<$Res> { + factory $DocumentStatePersistenceSettingsCopyWith(DocumentStatePersistenceSettings value, $Res Function(DocumentStatePersistenceSettings) _then) = _$DocumentStatePersistenceSettingsCopyWithImpl; +@useResult +$Res call({ + bool enabled, bool page, bool camera, bool locks, bool tool, bool navigator, bool layers, bool areas, int maxEntries, int maxAgeDays +}); + + + + +} +/// @nodoc +class _$DocumentStatePersistenceSettingsCopyWithImpl<$Res> + implements $DocumentStatePersistenceSettingsCopyWith<$Res> { + _$DocumentStatePersistenceSettingsCopyWithImpl(this._self, this._then); + + final DocumentStatePersistenceSettings _self; + final $Res Function(DocumentStatePersistenceSettings) _then; + +/// Create a copy of DocumentStatePersistenceSettings +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? enabled = null,Object? page = null,Object? camera = null,Object? locks = null,Object? tool = null,Object? navigator = null,Object? layers = null,Object? areas = null,Object? maxEntries = null,Object? maxAgeDays = null,}) { + return _then(_self.copyWith( +enabled: null == enabled ? _self.enabled : enabled // ignore: cast_nullable_to_non_nullable +as bool,page: null == page ? _self.page : page // ignore: cast_nullable_to_non_nullable +as bool,camera: null == camera ? _self.camera : camera // ignore: cast_nullable_to_non_nullable +as bool,locks: null == locks ? _self.locks : locks // ignore: cast_nullable_to_non_nullable +as bool,tool: null == tool ? _self.tool : tool // ignore: cast_nullable_to_non_nullable +as bool,navigator: null == navigator ? _self.navigator : navigator // ignore: cast_nullable_to_non_nullable +as bool,layers: null == layers ? _self.layers : layers // ignore: cast_nullable_to_non_nullable +as bool,areas: null == areas ? _self.areas : areas // ignore: cast_nullable_to_non_nullable +as bool,maxEntries: null == maxEntries ? _self.maxEntries : maxEntries // ignore: cast_nullable_to_non_nullable +as int,maxAgeDays: null == maxAgeDays ? _self.maxAgeDays : maxAgeDays // ignore: cast_nullable_to_non_nullable +as int, + )); +} + +} + + + +/// @nodoc +@JsonSerializable() + +class _DocumentStatePersistenceSettings with DiagnosticableTreeMixin implements DocumentStatePersistenceSettings { + const _DocumentStatePersistenceSettings({this.enabled = true, this.page = true, this.camera = true, this.locks = true, this.tool = true, this.navigator = true, this.layers = true, this.areas = true, this.maxEntries = 400, this.maxAgeDays = 180}); + factory _DocumentStatePersistenceSettings.fromJson(Map json) => _$DocumentStatePersistenceSettingsFromJson(json); + +@override@JsonKey() final bool enabled; +@override@JsonKey() final bool page; +@override@JsonKey() final bool camera; +@override@JsonKey() final bool locks; +@override@JsonKey() final bool tool; +@override@JsonKey() final bool navigator; +@override@JsonKey() final bool layers; +@override@JsonKey() final bool areas; +@override@JsonKey() final int maxEntries; +@override@JsonKey() final int maxAgeDays; + +/// Create a copy of DocumentStatePersistenceSettings +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$DocumentStatePersistenceSettingsCopyWith<_DocumentStatePersistenceSettings> get copyWith => __$DocumentStatePersistenceSettingsCopyWithImpl<_DocumentStatePersistenceSettings>(this, _$identity); + +@override +Map toJson() { + return _$DocumentStatePersistenceSettingsToJson(this, ); +} +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'DocumentStatePersistenceSettings')) + ..add(DiagnosticsProperty('enabled', enabled))..add(DiagnosticsProperty('page', page))..add(DiagnosticsProperty('camera', camera))..add(DiagnosticsProperty('locks', locks))..add(DiagnosticsProperty('tool', tool))..add(DiagnosticsProperty('navigator', navigator))..add(DiagnosticsProperty('layers', layers))..add(DiagnosticsProperty('areas', areas))..add(DiagnosticsProperty('maxEntries', maxEntries))..add(DiagnosticsProperty('maxAgeDays', maxAgeDays)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _DocumentStatePersistenceSettings&&(identical(other.enabled, enabled) || other.enabled == enabled)&&(identical(other.page, page) || other.page == page)&&(identical(other.camera, camera) || other.camera == camera)&&(identical(other.locks, locks) || other.locks == locks)&&(identical(other.tool, tool) || other.tool == tool)&&(identical(other.navigator, navigator) || other.navigator == navigator)&&(identical(other.layers, layers) || other.layers == layers)&&(identical(other.areas, areas) || other.areas == areas)&&(identical(other.maxEntries, maxEntries) || other.maxEntries == maxEntries)&&(identical(other.maxAgeDays, maxAgeDays) || other.maxAgeDays == maxAgeDays)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,enabled,page,camera,locks,tool,navigator,layers,areas,maxEntries,maxAgeDays); + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'DocumentStatePersistenceSettings(enabled: $enabled, page: $page, camera: $camera, locks: $locks, tool: $tool, navigator: $navigator, layers: $layers, areas: $areas, maxEntries: $maxEntries, maxAgeDays: $maxAgeDays)'; +} + + +} + +/// @nodoc +abstract mixin class _$DocumentStatePersistenceSettingsCopyWith<$Res> implements $DocumentStatePersistenceSettingsCopyWith<$Res> { + factory _$DocumentStatePersistenceSettingsCopyWith(_DocumentStatePersistenceSettings value, $Res Function(_DocumentStatePersistenceSettings) _then) = __$DocumentStatePersistenceSettingsCopyWithImpl; +@override @useResult +$Res call({ + bool enabled, bool page, bool camera, bool locks, bool tool, bool navigator, bool layers, bool areas, int maxEntries, int maxAgeDays +}); + + + + +} +/// @nodoc +class __$DocumentStatePersistenceSettingsCopyWithImpl<$Res> + implements _$DocumentStatePersistenceSettingsCopyWith<$Res> { + __$DocumentStatePersistenceSettingsCopyWithImpl(this._self, this._then); + + final _DocumentStatePersistenceSettings _self; + final $Res Function(_DocumentStatePersistenceSettings) _then; + +/// Create a copy of DocumentStatePersistenceSettings +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? enabled = null,Object? page = null,Object? camera = null,Object? locks = null,Object? tool = null,Object? navigator = null,Object? layers = null,Object? areas = null,Object? maxEntries = null,Object? maxAgeDays = null,}) { + return _then(_DocumentStatePersistenceSettings( +enabled: null == enabled ? _self.enabled : enabled // ignore: cast_nullable_to_non_nullable +as bool,page: null == page ? _self.page : page // ignore: cast_nullable_to_non_nullable +as bool,camera: null == camera ? _self.camera : camera // ignore: cast_nullable_to_non_nullable +as bool,locks: null == locks ? _self.locks : locks // ignore: cast_nullable_to_non_nullable +as bool,tool: null == tool ? _self.tool : tool // ignore: cast_nullable_to_non_nullable +as bool,navigator: null == navigator ? _self.navigator : navigator // ignore: cast_nullable_to_non_nullable +as bool,layers: null == layers ? _self.layers : layers // ignore: cast_nullable_to_non_nullable +as bool,areas: null == areas ? _self.areas : areas // ignore: cast_nullable_to_non_nullable +as bool,maxEntries: null == maxEntries ? _self.maxEntries : maxEntries // ignore: cast_nullable_to_non_nullable +as int,maxAgeDays: null == maxAgeDays ? _self.maxAgeDays : maxAgeDays // ignore: cast_nullable_to_non_nullable +as int, + )); +} + + +} + + /// @nodoc mixin _$ButterflySettings implements DiagnosticableTreeMixin { - ThemeMode get theme; ThemeDensity get density; double? get limitViewportMultiplier; bool get limitViewportPositive; String get localeTag; String get documentPath; double get gestureSensitivity; double get touchSensitivity; double get selectSensitivity; double get scrollSensitivity; bool? get penOnlyInput; bool get showPenOnlyToggle; bool get inputGestures; String get design; BannerVisibility get bannerVisibility;@JsonKey(includeFromJson: false, includeToJson: false) List get history; bool get zoomEnabled; ZoomPosition get zoomPosition; ZoomPosition get propertyPosition; String? get lastVersion;@JsonKey(includeFromJson: false, includeToJson: false) List get connections; String get defaultRemote; bool get nativeTitleBar; bool get startInFullScreen; bool get navigationRail; IgnorePressure get ignorePressure; SyncMode get syncMode; InputConfiguration get inputConfiguration; String get fallbackPack; List get starred; List get favoriteTemplates; String get defaultTemplate; NavigatorPosition get navigatorPosition; ToolbarPosition get toolbarPosition; ToolbarSize get toolbarSize; SortBy get sortBy; SortOrder get sortOrder; double get imageScale; PlatformTheme get platformTheme;@SRGBConverter() List get recentColors; List get flags; bool get spreadPages; bool get highContrast; bool get gridView; bool get hideExtension; bool get autosave; bool get showSaveButton; int get toolbarRows; bool get delayedAutosave; int get autosaveDelaySeconds; bool get hideCursorWhileDrawing; StartupBehavior get onStartup; SimpleToolbarVisibility get simpleToolbarVisibility; OptionsPanelPosition get optionsPanelPosition; RenderResolution get renderResolution; bool get moveOnGesture; List get swamps; PackAssetLocation? get selectedPalette; bool get showVerboseLogs; bool get showThumbnails; bool get bringMovedElementsToFront; List get favoriteTools; + ThemeMode get theme; ThemeDensity get density; double? get limitViewportMultiplier; bool get limitViewportPositive; String get localeTag; String get documentPath; double get gestureSensitivity; double get touchSensitivity; double get selectSensitivity; double get scrollSensitivity; bool? get penOnlyInput; bool get showPenOnlyToggle; bool get inputGestures; String get design; BannerVisibility get bannerVisibility;@JsonKey(includeFromJson: false, includeToJson: false) List get history; bool get zoomEnabled; ZoomPosition get zoomPosition; ZoomPosition get propertyPosition; String? get lastVersion;@JsonKey(includeFromJson: false, includeToJson: false) List get connections; String get defaultRemote; bool get nativeTitleBar; bool get startInFullScreen; bool get navigationRail; IgnorePressure get ignorePressure; SyncMode get syncMode; InputConfiguration get inputConfiguration; String get fallbackPack; List get starred; List get favoriteTemplates; String get defaultTemplate; NavigatorPosition get navigatorPosition; ToolbarPosition get toolbarPosition; ToolbarSize get toolbarSize; SortBy get sortBy; SortOrder get sortOrder; double get imageScale; PlatformTheme get platformTheme;@SRGBConverter() List get recentColors; List get flags; bool get spreadPages; bool get highContrast; bool get gridView; bool get hideExtension; bool get autosave; bool get showSaveButton; int get toolbarRows; bool get delayedAutosave; int get autosaveDelaySeconds; bool get hideCursorWhileDrawing; StartupBehavior get onStartup; DocumentStatePersistenceSettings get documentStatePersistence; SimpleToolbarVisibility get simpleToolbarVisibility; OptionsPanelPosition get optionsPanelPosition; RenderResolution get renderResolution; bool get moveOnGesture; List get swamps; PackAssetLocation? get selectedPalette; bool get showVerboseLogs; bool get showThumbnails; bool get bringMovedElementsToFront; List get favoriteTools; /// Create a copy of ButterflySettings /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -551,21 +724,21 @@ $ButterflySettingsCopyWith get copyWith => _$ButterflySetting void debugFillProperties(DiagnosticPropertiesBuilder properties) { properties ..add(DiagnosticsProperty('type', 'ButterflySettings')) - ..add(DiagnosticsProperty('theme', theme))..add(DiagnosticsProperty('density', density))..add(DiagnosticsProperty('limitViewportMultiplier', limitViewportMultiplier))..add(DiagnosticsProperty('limitViewportPositive', limitViewportPositive))..add(DiagnosticsProperty('localeTag', localeTag))..add(DiagnosticsProperty('documentPath', documentPath))..add(DiagnosticsProperty('gestureSensitivity', gestureSensitivity))..add(DiagnosticsProperty('touchSensitivity', touchSensitivity))..add(DiagnosticsProperty('selectSensitivity', selectSensitivity))..add(DiagnosticsProperty('scrollSensitivity', scrollSensitivity))..add(DiagnosticsProperty('penOnlyInput', penOnlyInput))..add(DiagnosticsProperty('showPenOnlyToggle', showPenOnlyToggle))..add(DiagnosticsProperty('inputGestures', inputGestures))..add(DiagnosticsProperty('design', design))..add(DiagnosticsProperty('bannerVisibility', bannerVisibility))..add(DiagnosticsProperty('history', history))..add(DiagnosticsProperty('zoomEnabled', zoomEnabled))..add(DiagnosticsProperty('zoomPosition', zoomPosition))..add(DiagnosticsProperty('propertyPosition', propertyPosition))..add(DiagnosticsProperty('lastVersion', lastVersion))..add(DiagnosticsProperty('connections', connections))..add(DiagnosticsProperty('defaultRemote', defaultRemote))..add(DiagnosticsProperty('nativeTitleBar', nativeTitleBar))..add(DiagnosticsProperty('startInFullScreen', startInFullScreen))..add(DiagnosticsProperty('navigationRail', navigationRail))..add(DiagnosticsProperty('ignorePressure', ignorePressure))..add(DiagnosticsProperty('syncMode', syncMode))..add(DiagnosticsProperty('inputConfiguration', inputConfiguration))..add(DiagnosticsProperty('fallbackPack', fallbackPack))..add(DiagnosticsProperty('starred', starred))..add(DiagnosticsProperty('favoriteTemplates', favoriteTemplates))..add(DiagnosticsProperty('defaultTemplate', defaultTemplate))..add(DiagnosticsProperty('navigatorPosition', navigatorPosition))..add(DiagnosticsProperty('toolbarPosition', toolbarPosition))..add(DiagnosticsProperty('toolbarSize', toolbarSize))..add(DiagnosticsProperty('sortBy', sortBy))..add(DiagnosticsProperty('sortOrder', sortOrder))..add(DiagnosticsProperty('imageScale', imageScale))..add(DiagnosticsProperty('platformTheme', platformTheme))..add(DiagnosticsProperty('recentColors', recentColors))..add(DiagnosticsProperty('flags', flags))..add(DiagnosticsProperty('spreadPages', spreadPages))..add(DiagnosticsProperty('highContrast', highContrast))..add(DiagnosticsProperty('gridView', gridView))..add(DiagnosticsProperty('hideExtension', hideExtension))..add(DiagnosticsProperty('autosave', autosave))..add(DiagnosticsProperty('showSaveButton', showSaveButton))..add(DiagnosticsProperty('toolbarRows', toolbarRows))..add(DiagnosticsProperty('delayedAutosave', delayedAutosave))..add(DiagnosticsProperty('autosaveDelaySeconds', autosaveDelaySeconds))..add(DiagnosticsProperty('hideCursorWhileDrawing', hideCursorWhileDrawing))..add(DiagnosticsProperty('onStartup', onStartup))..add(DiagnosticsProperty('simpleToolbarVisibility', simpleToolbarVisibility))..add(DiagnosticsProperty('optionsPanelPosition', optionsPanelPosition))..add(DiagnosticsProperty('renderResolution', renderResolution))..add(DiagnosticsProperty('moveOnGesture', moveOnGesture))..add(DiagnosticsProperty('swamps', swamps))..add(DiagnosticsProperty('selectedPalette', selectedPalette))..add(DiagnosticsProperty('showVerboseLogs', showVerboseLogs))..add(DiagnosticsProperty('showThumbnails', showThumbnails))..add(DiagnosticsProperty('bringMovedElementsToFront', bringMovedElementsToFront))..add(DiagnosticsProperty('favoriteTools', favoriteTools)); + ..add(DiagnosticsProperty('theme', theme))..add(DiagnosticsProperty('density', density))..add(DiagnosticsProperty('limitViewportMultiplier', limitViewportMultiplier))..add(DiagnosticsProperty('limitViewportPositive', limitViewportPositive))..add(DiagnosticsProperty('localeTag', localeTag))..add(DiagnosticsProperty('documentPath', documentPath))..add(DiagnosticsProperty('gestureSensitivity', gestureSensitivity))..add(DiagnosticsProperty('touchSensitivity', touchSensitivity))..add(DiagnosticsProperty('selectSensitivity', selectSensitivity))..add(DiagnosticsProperty('scrollSensitivity', scrollSensitivity))..add(DiagnosticsProperty('penOnlyInput', penOnlyInput))..add(DiagnosticsProperty('showPenOnlyToggle', showPenOnlyToggle))..add(DiagnosticsProperty('inputGestures', inputGestures))..add(DiagnosticsProperty('design', design))..add(DiagnosticsProperty('bannerVisibility', bannerVisibility))..add(DiagnosticsProperty('history', history))..add(DiagnosticsProperty('zoomEnabled', zoomEnabled))..add(DiagnosticsProperty('zoomPosition', zoomPosition))..add(DiagnosticsProperty('propertyPosition', propertyPosition))..add(DiagnosticsProperty('lastVersion', lastVersion))..add(DiagnosticsProperty('connections', connections))..add(DiagnosticsProperty('defaultRemote', defaultRemote))..add(DiagnosticsProperty('nativeTitleBar', nativeTitleBar))..add(DiagnosticsProperty('startInFullScreen', startInFullScreen))..add(DiagnosticsProperty('navigationRail', navigationRail))..add(DiagnosticsProperty('ignorePressure', ignorePressure))..add(DiagnosticsProperty('syncMode', syncMode))..add(DiagnosticsProperty('inputConfiguration', inputConfiguration))..add(DiagnosticsProperty('fallbackPack', fallbackPack))..add(DiagnosticsProperty('starred', starred))..add(DiagnosticsProperty('favoriteTemplates', favoriteTemplates))..add(DiagnosticsProperty('defaultTemplate', defaultTemplate))..add(DiagnosticsProperty('navigatorPosition', navigatorPosition))..add(DiagnosticsProperty('toolbarPosition', toolbarPosition))..add(DiagnosticsProperty('toolbarSize', toolbarSize))..add(DiagnosticsProperty('sortBy', sortBy))..add(DiagnosticsProperty('sortOrder', sortOrder))..add(DiagnosticsProperty('imageScale', imageScale))..add(DiagnosticsProperty('platformTheme', platformTheme))..add(DiagnosticsProperty('recentColors', recentColors))..add(DiagnosticsProperty('flags', flags))..add(DiagnosticsProperty('spreadPages', spreadPages))..add(DiagnosticsProperty('highContrast', highContrast))..add(DiagnosticsProperty('gridView', gridView))..add(DiagnosticsProperty('hideExtension', hideExtension))..add(DiagnosticsProperty('autosave', autosave))..add(DiagnosticsProperty('showSaveButton', showSaveButton))..add(DiagnosticsProperty('toolbarRows', toolbarRows))..add(DiagnosticsProperty('delayedAutosave', delayedAutosave))..add(DiagnosticsProperty('autosaveDelaySeconds', autosaveDelaySeconds))..add(DiagnosticsProperty('hideCursorWhileDrawing', hideCursorWhileDrawing))..add(DiagnosticsProperty('onStartup', onStartup))..add(DiagnosticsProperty('documentStatePersistence', documentStatePersistence))..add(DiagnosticsProperty('simpleToolbarVisibility', simpleToolbarVisibility))..add(DiagnosticsProperty('optionsPanelPosition', optionsPanelPosition))..add(DiagnosticsProperty('renderResolution', renderResolution))..add(DiagnosticsProperty('moveOnGesture', moveOnGesture))..add(DiagnosticsProperty('swamps', swamps))..add(DiagnosticsProperty('selectedPalette', selectedPalette))..add(DiagnosticsProperty('showVerboseLogs', showVerboseLogs))..add(DiagnosticsProperty('showThumbnails', showThumbnails))..add(DiagnosticsProperty('bringMovedElementsToFront', bringMovedElementsToFront))..add(DiagnosticsProperty('favoriteTools', favoriteTools)); } @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ButterflySettings&&(identical(other.theme, theme) || other.theme == theme)&&(identical(other.density, density) || other.density == density)&&(identical(other.limitViewportMultiplier, limitViewportMultiplier) || other.limitViewportMultiplier == limitViewportMultiplier)&&(identical(other.limitViewportPositive, limitViewportPositive) || other.limitViewportPositive == limitViewportPositive)&&(identical(other.localeTag, localeTag) || other.localeTag == localeTag)&&(identical(other.documentPath, documentPath) || other.documentPath == documentPath)&&(identical(other.gestureSensitivity, gestureSensitivity) || other.gestureSensitivity == gestureSensitivity)&&(identical(other.touchSensitivity, touchSensitivity) || other.touchSensitivity == touchSensitivity)&&(identical(other.selectSensitivity, selectSensitivity) || other.selectSensitivity == selectSensitivity)&&(identical(other.scrollSensitivity, scrollSensitivity) || other.scrollSensitivity == scrollSensitivity)&&(identical(other.penOnlyInput, penOnlyInput) || other.penOnlyInput == penOnlyInput)&&(identical(other.showPenOnlyToggle, showPenOnlyToggle) || other.showPenOnlyToggle == showPenOnlyToggle)&&(identical(other.inputGestures, inputGestures) || other.inputGestures == inputGestures)&&(identical(other.design, design) || other.design == design)&&(identical(other.bannerVisibility, bannerVisibility) || other.bannerVisibility == bannerVisibility)&&const DeepCollectionEquality().equals(other.history, history)&&(identical(other.zoomEnabled, zoomEnabled) || other.zoomEnabled == zoomEnabled)&&(identical(other.zoomPosition, zoomPosition) || other.zoomPosition == zoomPosition)&&(identical(other.propertyPosition, propertyPosition) || other.propertyPosition == propertyPosition)&&(identical(other.lastVersion, lastVersion) || other.lastVersion == lastVersion)&&const DeepCollectionEquality().equals(other.connections, connections)&&(identical(other.defaultRemote, defaultRemote) || other.defaultRemote == defaultRemote)&&(identical(other.nativeTitleBar, nativeTitleBar) || other.nativeTitleBar == nativeTitleBar)&&(identical(other.startInFullScreen, startInFullScreen) || other.startInFullScreen == startInFullScreen)&&(identical(other.navigationRail, navigationRail) || other.navigationRail == navigationRail)&&(identical(other.ignorePressure, ignorePressure) || other.ignorePressure == ignorePressure)&&(identical(other.syncMode, syncMode) || other.syncMode == syncMode)&&(identical(other.inputConfiguration, inputConfiguration) || other.inputConfiguration == inputConfiguration)&&(identical(other.fallbackPack, fallbackPack) || other.fallbackPack == fallbackPack)&&const DeepCollectionEquality().equals(other.starred, starred)&&const DeepCollectionEquality().equals(other.favoriteTemplates, favoriteTemplates)&&(identical(other.defaultTemplate, defaultTemplate) || other.defaultTemplate == defaultTemplate)&&(identical(other.navigatorPosition, navigatorPosition) || other.navigatorPosition == navigatorPosition)&&(identical(other.toolbarPosition, toolbarPosition) || other.toolbarPosition == toolbarPosition)&&(identical(other.toolbarSize, toolbarSize) || other.toolbarSize == toolbarSize)&&(identical(other.sortBy, sortBy) || other.sortBy == sortBy)&&(identical(other.sortOrder, sortOrder) || other.sortOrder == sortOrder)&&(identical(other.imageScale, imageScale) || other.imageScale == imageScale)&&(identical(other.platformTheme, platformTheme) || other.platformTheme == platformTheme)&&const DeepCollectionEquality().equals(other.recentColors, recentColors)&&const DeepCollectionEquality().equals(other.flags, flags)&&(identical(other.spreadPages, spreadPages) || other.spreadPages == spreadPages)&&(identical(other.highContrast, highContrast) || other.highContrast == highContrast)&&(identical(other.gridView, gridView) || other.gridView == gridView)&&(identical(other.hideExtension, hideExtension) || other.hideExtension == hideExtension)&&(identical(other.autosave, autosave) || other.autosave == autosave)&&(identical(other.showSaveButton, showSaveButton) || other.showSaveButton == showSaveButton)&&(identical(other.toolbarRows, toolbarRows) || other.toolbarRows == toolbarRows)&&(identical(other.delayedAutosave, delayedAutosave) || other.delayedAutosave == delayedAutosave)&&(identical(other.autosaveDelaySeconds, autosaveDelaySeconds) || other.autosaveDelaySeconds == autosaveDelaySeconds)&&(identical(other.hideCursorWhileDrawing, hideCursorWhileDrawing) || other.hideCursorWhileDrawing == hideCursorWhileDrawing)&&(identical(other.onStartup, onStartup) || other.onStartup == onStartup)&&(identical(other.simpleToolbarVisibility, simpleToolbarVisibility) || other.simpleToolbarVisibility == simpleToolbarVisibility)&&(identical(other.optionsPanelPosition, optionsPanelPosition) || other.optionsPanelPosition == optionsPanelPosition)&&(identical(other.renderResolution, renderResolution) || other.renderResolution == renderResolution)&&(identical(other.moveOnGesture, moveOnGesture) || other.moveOnGesture == moveOnGesture)&&const DeepCollectionEquality().equals(other.swamps, swamps)&&(identical(other.selectedPalette, selectedPalette) || other.selectedPalette == selectedPalette)&&(identical(other.showVerboseLogs, showVerboseLogs) || other.showVerboseLogs == showVerboseLogs)&&(identical(other.showThumbnails, showThumbnails) || other.showThumbnails == showThumbnails)&&(identical(other.bringMovedElementsToFront, bringMovedElementsToFront) || other.bringMovedElementsToFront == bringMovedElementsToFront)&&const DeepCollectionEquality().equals(other.favoriteTools, favoriteTools)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is ButterflySettings&&(identical(other.theme, theme) || other.theme == theme)&&(identical(other.density, density) || other.density == density)&&(identical(other.limitViewportMultiplier, limitViewportMultiplier) || other.limitViewportMultiplier == limitViewportMultiplier)&&(identical(other.limitViewportPositive, limitViewportPositive) || other.limitViewportPositive == limitViewportPositive)&&(identical(other.localeTag, localeTag) || other.localeTag == localeTag)&&(identical(other.documentPath, documentPath) || other.documentPath == documentPath)&&(identical(other.gestureSensitivity, gestureSensitivity) || other.gestureSensitivity == gestureSensitivity)&&(identical(other.touchSensitivity, touchSensitivity) || other.touchSensitivity == touchSensitivity)&&(identical(other.selectSensitivity, selectSensitivity) || other.selectSensitivity == selectSensitivity)&&(identical(other.scrollSensitivity, scrollSensitivity) || other.scrollSensitivity == scrollSensitivity)&&(identical(other.penOnlyInput, penOnlyInput) || other.penOnlyInput == penOnlyInput)&&(identical(other.showPenOnlyToggle, showPenOnlyToggle) || other.showPenOnlyToggle == showPenOnlyToggle)&&(identical(other.inputGestures, inputGestures) || other.inputGestures == inputGestures)&&(identical(other.design, design) || other.design == design)&&(identical(other.bannerVisibility, bannerVisibility) || other.bannerVisibility == bannerVisibility)&&const DeepCollectionEquality().equals(other.history, history)&&(identical(other.zoomEnabled, zoomEnabled) || other.zoomEnabled == zoomEnabled)&&(identical(other.zoomPosition, zoomPosition) || other.zoomPosition == zoomPosition)&&(identical(other.propertyPosition, propertyPosition) || other.propertyPosition == propertyPosition)&&(identical(other.lastVersion, lastVersion) || other.lastVersion == lastVersion)&&const DeepCollectionEquality().equals(other.connections, connections)&&(identical(other.defaultRemote, defaultRemote) || other.defaultRemote == defaultRemote)&&(identical(other.nativeTitleBar, nativeTitleBar) || other.nativeTitleBar == nativeTitleBar)&&(identical(other.startInFullScreen, startInFullScreen) || other.startInFullScreen == startInFullScreen)&&(identical(other.navigationRail, navigationRail) || other.navigationRail == navigationRail)&&(identical(other.ignorePressure, ignorePressure) || other.ignorePressure == ignorePressure)&&(identical(other.syncMode, syncMode) || other.syncMode == syncMode)&&(identical(other.inputConfiguration, inputConfiguration) || other.inputConfiguration == inputConfiguration)&&(identical(other.fallbackPack, fallbackPack) || other.fallbackPack == fallbackPack)&&const DeepCollectionEquality().equals(other.starred, starred)&&const DeepCollectionEquality().equals(other.favoriteTemplates, favoriteTemplates)&&(identical(other.defaultTemplate, defaultTemplate) || other.defaultTemplate == defaultTemplate)&&(identical(other.navigatorPosition, navigatorPosition) || other.navigatorPosition == navigatorPosition)&&(identical(other.toolbarPosition, toolbarPosition) || other.toolbarPosition == toolbarPosition)&&(identical(other.toolbarSize, toolbarSize) || other.toolbarSize == toolbarSize)&&(identical(other.sortBy, sortBy) || other.sortBy == sortBy)&&(identical(other.sortOrder, sortOrder) || other.sortOrder == sortOrder)&&(identical(other.imageScale, imageScale) || other.imageScale == imageScale)&&(identical(other.platformTheme, platformTheme) || other.platformTheme == platformTheme)&&const DeepCollectionEquality().equals(other.recentColors, recentColors)&&const DeepCollectionEquality().equals(other.flags, flags)&&(identical(other.spreadPages, spreadPages) || other.spreadPages == spreadPages)&&(identical(other.highContrast, highContrast) || other.highContrast == highContrast)&&(identical(other.gridView, gridView) || other.gridView == gridView)&&(identical(other.hideExtension, hideExtension) || other.hideExtension == hideExtension)&&(identical(other.autosave, autosave) || other.autosave == autosave)&&(identical(other.showSaveButton, showSaveButton) || other.showSaveButton == showSaveButton)&&(identical(other.toolbarRows, toolbarRows) || other.toolbarRows == toolbarRows)&&(identical(other.delayedAutosave, delayedAutosave) || other.delayedAutosave == delayedAutosave)&&(identical(other.autosaveDelaySeconds, autosaveDelaySeconds) || other.autosaveDelaySeconds == autosaveDelaySeconds)&&(identical(other.hideCursorWhileDrawing, hideCursorWhileDrawing) || other.hideCursorWhileDrawing == hideCursorWhileDrawing)&&(identical(other.onStartup, onStartup) || other.onStartup == onStartup)&&(identical(other.documentStatePersistence, documentStatePersistence) || other.documentStatePersistence == documentStatePersistence)&&(identical(other.simpleToolbarVisibility, simpleToolbarVisibility) || other.simpleToolbarVisibility == simpleToolbarVisibility)&&(identical(other.optionsPanelPosition, optionsPanelPosition) || other.optionsPanelPosition == optionsPanelPosition)&&(identical(other.renderResolution, renderResolution) || other.renderResolution == renderResolution)&&(identical(other.moveOnGesture, moveOnGesture) || other.moveOnGesture == moveOnGesture)&&const DeepCollectionEquality().equals(other.swamps, swamps)&&(identical(other.selectedPalette, selectedPalette) || other.selectedPalette == selectedPalette)&&(identical(other.showVerboseLogs, showVerboseLogs) || other.showVerboseLogs == showVerboseLogs)&&(identical(other.showThumbnails, showThumbnails) || other.showThumbnails == showThumbnails)&&(identical(other.bringMovedElementsToFront, bringMovedElementsToFront) || other.bringMovedElementsToFront == bringMovedElementsToFront)&&const DeepCollectionEquality().equals(other.favoriteTools, favoriteTools)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hashAll([runtimeType,theme,density,limitViewportMultiplier,limitViewportPositive,localeTag,documentPath,gestureSensitivity,touchSensitivity,selectSensitivity,scrollSensitivity,penOnlyInput,showPenOnlyToggle,inputGestures,design,bannerVisibility,const DeepCollectionEquality().hash(history),zoomEnabled,zoomPosition,propertyPosition,lastVersion,const DeepCollectionEquality().hash(connections),defaultRemote,nativeTitleBar,startInFullScreen,navigationRail,ignorePressure,syncMode,inputConfiguration,fallbackPack,const DeepCollectionEquality().hash(starred),const DeepCollectionEquality().hash(favoriteTemplates),defaultTemplate,navigatorPosition,toolbarPosition,toolbarSize,sortBy,sortOrder,imageScale,platformTheme,const DeepCollectionEquality().hash(recentColors),const DeepCollectionEquality().hash(flags),spreadPages,highContrast,gridView,hideExtension,autosave,showSaveButton,toolbarRows,delayedAutosave,autosaveDelaySeconds,hideCursorWhileDrawing,onStartup,simpleToolbarVisibility,optionsPanelPosition,renderResolution,moveOnGesture,const DeepCollectionEquality().hash(swamps),selectedPalette,showVerboseLogs,showThumbnails,bringMovedElementsToFront,const DeepCollectionEquality().hash(favoriteTools)]); +int get hashCode => Object.hashAll([runtimeType,theme,density,limitViewportMultiplier,limitViewportPositive,localeTag,documentPath,gestureSensitivity,touchSensitivity,selectSensitivity,scrollSensitivity,penOnlyInput,showPenOnlyToggle,inputGestures,design,bannerVisibility,const DeepCollectionEquality().hash(history),zoomEnabled,zoomPosition,propertyPosition,lastVersion,const DeepCollectionEquality().hash(connections),defaultRemote,nativeTitleBar,startInFullScreen,navigationRail,ignorePressure,syncMode,inputConfiguration,fallbackPack,const DeepCollectionEquality().hash(starred),const DeepCollectionEquality().hash(favoriteTemplates),defaultTemplate,navigatorPosition,toolbarPosition,toolbarSize,sortBy,sortOrder,imageScale,platformTheme,const DeepCollectionEquality().hash(recentColors),const DeepCollectionEquality().hash(flags),spreadPages,highContrast,gridView,hideExtension,autosave,showSaveButton,toolbarRows,delayedAutosave,autosaveDelaySeconds,hideCursorWhileDrawing,onStartup,documentStatePersistence,simpleToolbarVisibility,optionsPanelPosition,renderResolution,moveOnGesture,const DeepCollectionEquality().hash(swamps),selectedPalette,showVerboseLogs,showThumbnails,bringMovedElementsToFront,const DeepCollectionEquality().hash(favoriteTools)]); @override String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'ButterflySettings(theme: $theme, density: $density, limitViewportMultiplier: $limitViewportMultiplier, limitViewportPositive: $limitViewportPositive, localeTag: $localeTag, documentPath: $documentPath, gestureSensitivity: $gestureSensitivity, touchSensitivity: $touchSensitivity, selectSensitivity: $selectSensitivity, scrollSensitivity: $scrollSensitivity, penOnlyInput: $penOnlyInput, showPenOnlyToggle: $showPenOnlyToggle, inputGestures: $inputGestures, design: $design, bannerVisibility: $bannerVisibility, history: $history, zoomEnabled: $zoomEnabled, zoomPosition: $zoomPosition, propertyPosition: $propertyPosition, lastVersion: $lastVersion, connections: $connections, defaultRemote: $defaultRemote, nativeTitleBar: $nativeTitleBar, startInFullScreen: $startInFullScreen, navigationRail: $navigationRail, ignorePressure: $ignorePressure, syncMode: $syncMode, inputConfiguration: $inputConfiguration, fallbackPack: $fallbackPack, starred: $starred, favoriteTemplates: $favoriteTemplates, defaultTemplate: $defaultTemplate, navigatorPosition: $navigatorPosition, toolbarPosition: $toolbarPosition, toolbarSize: $toolbarSize, sortBy: $sortBy, sortOrder: $sortOrder, imageScale: $imageScale, platformTheme: $platformTheme, recentColors: $recentColors, flags: $flags, spreadPages: $spreadPages, highContrast: $highContrast, gridView: $gridView, hideExtension: $hideExtension, autosave: $autosave, showSaveButton: $showSaveButton, toolbarRows: $toolbarRows, delayedAutosave: $delayedAutosave, autosaveDelaySeconds: $autosaveDelaySeconds, hideCursorWhileDrawing: $hideCursorWhileDrawing, onStartup: $onStartup, simpleToolbarVisibility: $simpleToolbarVisibility, optionsPanelPosition: $optionsPanelPosition, renderResolution: $renderResolution, moveOnGesture: $moveOnGesture, swamps: $swamps, selectedPalette: $selectedPalette, showVerboseLogs: $showVerboseLogs, showThumbnails: $showThumbnails, bringMovedElementsToFront: $bringMovedElementsToFront, favoriteTools: $favoriteTools)'; + return 'ButterflySettings(theme: $theme, density: $density, limitViewportMultiplier: $limitViewportMultiplier, limitViewportPositive: $limitViewportPositive, localeTag: $localeTag, documentPath: $documentPath, gestureSensitivity: $gestureSensitivity, touchSensitivity: $touchSensitivity, selectSensitivity: $selectSensitivity, scrollSensitivity: $scrollSensitivity, penOnlyInput: $penOnlyInput, showPenOnlyToggle: $showPenOnlyToggle, inputGestures: $inputGestures, design: $design, bannerVisibility: $bannerVisibility, history: $history, zoomEnabled: $zoomEnabled, zoomPosition: $zoomPosition, propertyPosition: $propertyPosition, lastVersion: $lastVersion, connections: $connections, defaultRemote: $defaultRemote, nativeTitleBar: $nativeTitleBar, startInFullScreen: $startInFullScreen, navigationRail: $navigationRail, ignorePressure: $ignorePressure, syncMode: $syncMode, inputConfiguration: $inputConfiguration, fallbackPack: $fallbackPack, starred: $starred, favoriteTemplates: $favoriteTemplates, defaultTemplate: $defaultTemplate, navigatorPosition: $navigatorPosition, toolbarPosition: $toolbarPosition, toolbarSize: $toolbarSize, sortBy: $sortBy, sortOrder: $sortOrder, imageScale: $imageScale, platformTheme: $platformTheme, recentColors: $recentColors, flags: $flags, spreadPages: $spreadPages, highContrast: $highContrast, gridView: $gridView, hideExtension: $hideExtension, autosave: $autosave, showSaveButton: $showSaveButton, toolbarRows: $toolbarRows, delayedAutosave: $delayedAutosave, autosaveDelaySeconds: $autosaveDelaySeconds, hideCursorWhileDrawing: $hideCursorWhileDrawing, onStartup: $onStartup, documentStatePersistence: $documentStatePersistence, simpleToolbarVisibility: $simpleToolbarVisibility, optionsPanelPosition: $optionsPanelPosition, renderResolution: $renderResolution, moveOnGesture: $moveOnGesture, swamps: $swamps, selectedPalette: $selectedPalette, showVerboseLogs: $showVerboseLogs, showThumbnails: $showThumbnails, bringMovedElementsToFront: $bringMovedElementsToFront, favoriteTools: $favoriteTools)'; } @@ -576,11 +749,11 @@ abstract mixin class $ButterflySettingsCopyWith<$Res> { factory $ButterflySettingsCopyWith(ButterflySettings value, $Res Function(ButterflySettings) _then) = _$ButterflySettingsCopyWithImpl; @useResult $Res call({ - ThemeMode theme, ThemeDensity density, double? limitViewportMultiplier, bool limitViewportPositive, String localeTag, String documentPath, double gestureSensitivity, double touchSensitivity, double selectSensitivity, double scrollSensitivity, bool? penOnlyInput, bool showPenOnlyToggle, bool inputGestures, String design, BannerVisibility bannerVisibility,@JsonKey(includeFromJson: false, includeToJson: false) List history, bool zoomEnabled, ZoomPosition zoomPosition, ZoomPosition propertyPosition, String? lastVersion,@JsonKey(includeFromJson: false, includeToJson: false) List connections, String defaultRemote, bool nativeTitleBar, bool startInFullScreen, bool navigationRail, IgnorePressure ignorePressure, SyncMode syncMode, InputConfiguration inputConfiguration, String fallbackPack, List starred, List favoriteTemplates, String defaultTemplate, NavigatorPosition navigatorPosition, ToolbarPosition toolbarPosition, ToolbarSize toolbarSize, SortBy sortBy, SortOrder sortOrder, double imageScale, PlatformTheme platformTheme,@SRGBConverter() List recentColors, List flags, bool spreadPages, bool highContrast, bool gridView, bool hideExtension, bool autosave, bool showSaveButton, int toolbarRows, bool delayedAutosave, int autosaveDelaySeconds, bool hideCursorWhileDrawing, StartupBehavior onStartup, SimpleToolbarVisibility simpleToolbarVisibility, OptionsPanelPosition optionsPanelPosition, RenderResolution renderResolution, bool moveOnGesture, List swamps, PackAssetLocation? selectedPalette, bool showVerboseLogs, bool showThumbnails, bool bringMovedElementsToFront, List favoriteTools + ThemeMode theme, ThemeDensity density, double? limitViewportMultiplier, bool limitViewportPositive, String localeTag, String documentPath, double gestureSensitivity, double touchSensitivity, double selectSensitivity, double scrollSensitivity, bool? penOnlyInput, bool showPenOnlyToggle, bool inputGestures, String design, BannerVisibility bannerVisibility,@JsonKey(includeFromJson: false, includeToJson: false) List history, bool zoomEnabled, ZoomPosition zoomPosition, ZoomPosition propertyPosition, String? lastVersion,@JsonKey(includeFromJson: false, includeToJson: false) List connections, String defaultRemote, bool nativeTitleBar, bool startInFullScreen, bool navigationRail, IgnorePressure ignorePressure, SyncMode syncMode, InputConfiguration inputConfiguration, String fallbackPack, List starred, List favoriteTemplates, String defaultTemplate, NavigatorPosition navigatorPosition, ToolbarPosition toolbarPosition, ToolbarSize toolbarSize, SortBy sortBy, SortOrder sortOrder, double imageScale, PlatformTheme platformTheme,@SRGBConverter() List recentColors, List flags, bool spreadPages, bool highContrast, bool gridView, bool hideExtension, bool autosave, bool showSaveButton, int toolbarRows, bool delayedAutosave, int autosaveDelaySeconds, bool hideCursorWhileDrawing, StartupBehavior onStartup, DocumentStatePersistenceSettings documentStatePersistence, SimpleToolbarVisibility simpleToolbarVisibility, OptionsPanelPosition optionsPanelPosition, RenderResolution renderResolution, bool moveOnGesture, List swamps, PackAssetLocation? selectedPalette, bool showVerboseLogs, bool showThumbnails, bool bringMovedElementsToFront, List favoriteTools }); -$InputConfigurationCopyWith<$Res> get inputConfiguration;$PackAssetLocationCopyWith<$Res>? get selectedPalette; +$InputConfigurationCopyWith<$Res> get inputConfiguration;$DocumentStatePersistenceSettingsCopyWith<$Res> get documentStatePersistence;$PackAssetLocationCopyWith<$Res>? get selectedPalette; } /// @nodoc @@ -593,7 +766,7 @@ class _$ButterflySettingsCopyWithImpl<$Res> /// Create a copy of ButterflySettings /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? theme = null,Object? density = null,Object? limitViewportMultiplier = freezed,Object? limitViewportPositive = null,Object? localeTag = null,Object? documentPath = null,Object? gestureSensitivity = null,Object? touchSensitivity = null,Object? selectSensitivity = null,Object? scrollSensitivity = null,Object? penOnlyInput = freezed,Object? showPenOnlyToggle = null,Object? inputGestures = null,Object? design = null,Object? bannerVisibility = null,Object? history = null,Object? zoomEnabled = null,Object? zoomPosition = null,Object? propertyPosition = null,Object? lastVersion = freezed,Object? connections = null,Object? defaultRemote = null,Object? nativeTitleBar = null,Object? startInFullScreen = null,Object? navigationRail = null,Object? ignorePressure = null,Object? syncMode = null,Object? inputConfiguration = null,Object? fallbackPack = null,Object? starred = null,Object? favoriteTemplates = null,Object? defaultTemplate = null,Object? navigatorPosition = null,Object? toolbarPosition = null,Object? toolbarSize = null,Object? sortBy = null,Object? sortOrder = null,Object? imageScale = null,Object? platformTheme = null,Object? recentColors = null,Object? flags = null,Object? spreadPages = null,Object? highContrast = null,Object? gridView = null,Object? hideExtension = null,Object? autosave = null,Object? showSaveButton = null,Object? toolbarRows = null,Object? delayedAutosave = null,Object? autosaveDelaySeconds = null,Object? hideCursorWhileDrawing = null,Object? onStartup = null,Object? simpleToolbarVisibility = null,Object? optionsPanelPosition = null,Object? renderResolution = null,Object? moveOnGesture = null,Object? swamps = null,Object? selectedPalette = freezed,Object? showVerboseLogs = null,Object? showThumbnails = null,Object? bringMovedElementsToFront = null,Object? favoriteTools = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? theme = null,Object? density = null,Object? limitViewportMultiplier = freezed,Object? limitViewportPositive = null,Object? localeTag = null,Object? documentPath = null,Object? gestureSensitivity = null,Object? touchSensitivity = null,Object? selectSensitivity = null,Object? scrollSensitivity = null,Object? penOnlyInput = freezed,Object? showPenOnlyToggle = null,Object? inputGestures = null,Object? design = null,Object? bannerVisibility = null,Object? history = null,Object? zoomEnabled = null,Object? zoomPosition = null,Object? propertyPosition = null,Object? lastVersion = freezed,Object? connections = null,Object? defaultRemote = null,Object? nativeTitleBar = null,Object? startInFullScreen = null,Object? navigationRail = null,Object? ignorePressure = null,Object? syncMode = null,Object? inputConfiguration = null,Object? fallbackPack = null,Object? starred = null,Object? favoriteTemplates = null,Object? defaultTemplate = null,Object? navigatorPosition = null,Object? toolbarPosition = null,Object? toolbarSize = null,Object? sortBy = null,Object? sortOrder = null,Object? imageScale = null,Object? platformTheme = null,Object? recentColors = null,Object? flags = null,Object? spreadPages = null,Object? highContrast = null,Object? gridView = null,Object? hideExtension = null,Object? autosave = null,Object? showSaveButton = null,Object? toolbarRows = null,Object? delayedAutosave = null,Object? autosaveDelaySeconds = null,Object? hideCursorWhileDrawing = null,Object? onStartup = null,Object? documentStatePersistence = null,Object? simpleToolbarVisibility = null,Object? optionsPanelPosition = null,Object? renderResolution = null,Object? moveOnGesture = null,Object? swamps = null,Object? selectedPalette = freezed,Object? showVerboseLogs = null,Object? showThumbnails = null,Object? bringMovedElementsToFront = null,Object? favoriteTools = null,}) { return _then(_self.copyWith( theme: null == theme ? _self.theme : theme // ignore: cast_nullable_to_non_nullable as ThemeMode,density: null == density ? _self.density : density // ignore: cast_nullable_to_non_nullable @@ -647,7 +820,8 @@ as int,delayedAutosave: null == delayedAutosave ? _self.delayedAutosave : delaye as bool,autosaveDelaySeconds: null == autosaveDelaySeconds ? _self.autosaveDelaySeconds : autosaveDelaySeconds // ignore: cast_nullable_to_non_nullable as int,hideCursorWhileDrawing: null == hideCursorWhileDrawing ? _self.hideCursorWhileDrawing : hideCursorWhileDrawing // ignore: cast_nullable_to_non_nullable as bool,onStartup: null == onStartup ? _self.onStartup : onStartup // ignore: cast_nullable_to_non_nullable -as StartupBehavior,simpleToolbarVisibility: null == simpleToolbarVisibility ? _self.simpleToolbarVisibility : simpleToolbarVisibility // ignore: cast_nullable_to_non_nullable +as StartupBehavior,documentStatePersistence: null == documentStatePersistence ? _self.documentStatePersistence : documentStatePersistence // ignore: cast_nullable_to_non_nullable +as DocumentStatePersistenceSettings,simpleToolbarVisibility: null == simpleToolbarVisibility ? _self.simpleToolbarVisibility : simpleToolbarVisibility // ignore: cast_nullable_to_non_nullable as SimpleToolbarVisibility,optionsPanelPosition: null == optionsPanelPosition ? _self.optionsPanelPosition : optionsPanelPosition // ignore: cast_nullable_to_non_nullable as OptionsPanelPosition,renderResolution: null == renderResolution ? _self.renderResolution : renderResolution // ignore: cast_nullable_to_non_nullable as RenderResolution,moveOnGesture: null == moveOnGesture ? _self.moveOnGesture : moveOnGesture // ignore: cast_nullable_to_non_nullable @@ -673,6 +847,15 @@ $InputConfigurationCopyWith<$Res> get inputConfiguration { /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') +$DocumentStatePersistenceSettingsCopyWith<$Res> get documentStatePersistence { + + return $DocumentStatePersistenceSettingsCopyWith<$Res>(_self.documentStatePersistence, (value) { + return _then(_self.copyWith(documentStatePersistence: value)); + }); +}/// Create a copy of ButterflySettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') $PackAssetLocationCopyWith<$Res>? get selectedPalette { if (_self.selectedPalette == null) { return null; @@ -690,7 +873,7 @@ $PackAssetLocationCopyWith<$Res>? get selectedPalette { @JsonSerializable() class _ButterflySettings extends ButterflySettings with DiagnosticableTreeMixin { - const _ButterflySettings({this.theme = ThemeMode.system, this.density = ThemeDensity.system, this.limitViewportMultiplier, this.limitViewportPositive = false, this.localeTag = '', this.documentPath = '', this.gestureSensitivity = 1, this.touchSensitivity = 1, this.selectSensitivity = 1, this.scrollSensitivity = 1, this.penOnlyInput, this.showPenOnlyToggle = true, this.inputGestures = true, this.design = '', this.bannerVisibility = BannerVisibility.always, @JsonKey(includeFromJson: false, includeToJson: false) final List history = const [], this.zoomEnabled = true, this.zoomPosition = ZoomPosition.bottomRight, this.propertyPosition = ZoomPosition.topRight, this.lastVersion, @JsonKey(includeFromJson: false, includeToJson: false) final List connections = const [], this.defaultRemote = '', this.nativeTitleBar = false, this.startInFullScreen = false, this.navigationRail = true, this.ignorePressure = IgnorePressure.first, this.syncMode = SyncMode.noMobile, this.inputConfiguration = const InputConfiguration(), this.fallbackPack = '', final List starred = const [], final List favoriteTemplates = const [], this.defaultTemplate = '', this.navigatorPosition = NavigatorPosition.left, this.toolbarPosition = ToolbarPosition.inline, this.toolbarSize = ToolbarSize.normal, this.sortBy = SortBy.modified, this.sortOrder = SortOrder.descending, this.imageScale = 0.5, this.platformTheme = PlatformTheme.system, @SRGBConverter() final List recentColors = const [], final List flags = const [], this.spreadPages = false, this.highContrast = false, this.gridView = false, this.hideExtension = true, this.autosave = true, this.showSaveButton = true, this.toolbarRows = 1, this.delayedAutosave = true, this.autosaveDelaySeconds = 3, this.hideCursorWhileDrawing = false, this.onStartup = StartupBehavior.openHomeScreen, this.simpleToolbarVisibility = SimpleToolbarVisibility.show, this.optionsPanelPosition = OptionsPanelPosition.top, this.renderResolution = RenderResolution.normal, this.moveOnGesture = true, final List swamps = const [], this.selectedPalette, this.showVerboseLogs = false, this.showThumbnails = true, this.bringMovedElementsToFront = false, final List favoriteTools = const []}): _history = history,_connections = connections,_starred = starred,_favoriteTemplates = favoriteTemplates,_recentColors = recentColors,_flags = flags,_swamps = swamps,_favoriteTools = favoriteTools,super._(); + const _ButterflySettings({this.theme = ThemeMode.system, this.density = ThemeDensity.system, this.limitViewportMultiplier, this.limitViewportPositive = false, this.localeTag = '', this.documentPath = '', this.gestureSensitivity = 1, this.touchSensitivity = 1, this.selectSensitivity = 1, this.scrollSensitivity = 1, this.penOnlyInput, this.showPenOnlyToggle = true, this.inputGestures = true, this.design = '', this.bannerVisibility = BannerVisibility.always, @JsonKey(includeFromJson: false, includeToJson: false) final List history = const [], this.zoomEnabled = true, this.zoomPosition = ZoomPosition.bottomRight, this.propertyPosition = ZoomPosition.topRight, this.lastVersion, @JsonKey(includeFromJson: false, includeToJson: false) final List connections = const [], this.defaultRemote = '', this.nativeTitleBar = false, this.startInFullScreen = false, this.navigationRail = true, this.ignorePressure = IgnorePressure.first, this.syncMode = SyncMode.noMobile, this.inputConfiguration = const InputConfiguration(), this.fallbackPack = '', final List starred = const [], final List favoriteTemplates = const [], this.defaultTemplate = '', this.navigatorPosition = NavigatorPosition.left, this.toolbarPosition = ToolbarPosition.inline, this.toolbarSize = ToolbarSize.normal, this.sortBy = SortBy.modified, this.sortOrder = SortOrder.descending, this.imageScale = 0.5, this.platformTheme = PlatformTheme.system, @SRGBConverter() final List recentColors = const [], final List flags = const [], this.spreadPages = false, this.highContrast = false, this.gridView = false, this.hideExtension = true, this.autosave = true, this.showSaveButton = true, this.toolbarRows = 1, this.delayedAutosave = true, this.autosaveDelaySeconds = 3, this.hideCursorWhileDrawing = false, this.onStartup = StartupBehavior.openHomeScreen, this.documentStatePersistence = const DocumentStatePersistenceSettings(), this.simpleToolbarVisibility = SimpleToolbarVisibility.show, this.optionsPanelPosition = OptionsPanelPosition.top, this.renderResolution = RenderResolution.normal, this.moveOnGesture = true, final List swamps = const [], this.selectedPalette, this.showVerboseLogs = false, this.showThumbnails = true, this.bringMovedElementsToFront = false, final List favoriteTools = const []}): _history = history,_connections = connections,_starred = starred,_favoriteTemplates = favoriteTemplates,_recentColors = recentColors,_flags = flags,_swamps = swamps,_favoriteTools = favoriteTools,super._(); factory _ButterflySettings.fromJson(Map json) => _$ButterflySettingsFromJson(json); @override@JsonKey() final ThemeMode theme; @@ -781,6 +964,7 @@ class _ButterflySettings extends ButterflySettings with DiagnosticableTreeMixin @override@JsonKey() final int autosaveDelaySeconds; @override@JsonKey() final bool hideCursorWhileDrawing; @override@JsonKey() final StartupBehavior onStartup; +@override@JsonKey() final DocumentStatePersistenceSettings documentStatePersistence; @override@JsonKey() final SimpleToolbarVisibility simpleToolbarVisibility; @override@JsonKey() final OptionsPanelPosition optionsPanelPosition; @override@JsonKey() final RenderResolution renderResolution; @@ -818,21 +1002,21 @@ Map toJson() { void debugFillProperties(DiagnosticPropertiesBuilder properties) { properties ..add(DiagnosticsProperty('type', 'ButterflySettings')) - ..add(DiagnosticsProperty('theme', theme))..add(DiagnosticsProperty('density', density))..add(DiagnosticsProperty('limitViewportMultiplier', limitViewportMultiplier))..add(DiagnosticsProperty('limitViewportPositive', limitViewportPositive))..add(DiagnosticsProperty('localeTag', localeTag))..add(DiagnosticsProperty('documentPath', documentPath))..add(DiagnosticsProperty('gestureSensitivity', gestureSensitivity))..add(DiagnosticsProperty('touchSensitivity', touchSensitivity))..add(DiagnosticsProperty('selectSensitivity', selectSensitivity))..add(DiagnosticsProperty('scrollSensitivity', scrollSensitivity))..add(DiagnosticsProperty('penOnlyInput', penOnlyInput))..add(DiagnosticsProperty('showPenOnlyToggle', showPenOnlyToggle))..add(DiagnosticsProperty('inputGestures', inputGestures))..add(DiagnosticsProperty('design', design))..add(DiagnosticsProperty('bannerVisibility', bannerVisibility))..add(DiagnosticsProperty('history', history))..add(DiagnosticsProperty('zoomEnabled', zoomEnabled))..add(DiagnosticsProperty('zoomPosition', zoomPosition))..add(DiagnosticsProperty('propertyPosition', propertyPosition))..add(DiagnosticsProperty('lastVersion', lastVersion))..add(DiagnosticsProperty('connections', connections))..add(DiagnosticsProperty('defaultRemote', defaultRemote))..add(DiagnosticsProperty('nativeTitleBar', nativeTitleBar))..add(DiagnosticsProperty('startInFullScreen', startInFullScreen))..add(DiagnosticsProperty('navigationRail', navigationRail))..add(DiagnosticsProperty('ignorePressure', ignorePressure))..add(DiagnosticsProperty('syncMode', syncMode))..add(DiagnosticsProperty('inputConfiguration', inputConfiguration))..add(DiagnosticsProperty('fallbackPack', fallbackPack))..add(DiagnosticsProperty('starred', starred))..add(DiagnosticsProperty('favoriteTemplates', favoriteTemplates))..add(DiagnosticsProperty('defaultTemplate', defaultTemplate))..add(DiagnosticsProperty('navigatorPosition', navigatorPosition))..add(DiagnosticsProperty('toolbarPosition', toolbarPosition))..add(DiagnosticsProperty('toolbarSize', toolbarSize))..add(DiagnosticsProperty('sortBy', sortBy))..add(DiagnosticsProperty('sortOrder', sortOrder))..add(DiagnosticsProperty('imageScale', imageScale))..add(DiagnosticsProperty('platformTheme', platformTheme))..add(DiagnosticsProperty('recentColors', recentColors))..add(DiagnosticsProperty('flags', flags))..add(DiagnosticsProperty('spreadPages', spreadPages))..add(DiagnosticsProperty('highContrast', highContrast))..add(DiagnosticsProperty('gridView', gridView))..add(DiagnosticsProperty('hideExtension', hideExtension))..add(DiagnosticsProperty('autosave', autosave))..add(DiagnosticsProperty('showSaveButton', showSaveButton))..add(DiagnosticsProperty('toolbarRows', toolbarRows))..add(DiagnosticsProperty('delayedAutosave', delayedAutosave))..add(DiagnosticsProperty('autosaveDelaySeconds', autosaveDelaySeconds))..add(DiagnosticsProperty('hideCursorWhileDrawing', hideCursorWhileDrawing))..add(DiagnosticsProperty('onStartup', onStartup))..add(DiagnosticsProperty('simpleToolbarVisibility', simpleToolbarVisibility))..add(DiagnosticsProperty('optionsPanelPosition', optionsPanelPosition))..add(DiagnosticsProperty('renderResolution', renderResolution))..add(DiagnosticsProperty('moveOnGesture', moveOnGesture))..add(DiagnosticsProperty('swamps', swamps))..add(DiagnosticsProperty('selectedPalette', selectedPalette))..add(DiagnosticsProperty('showVerboseLogs', showVerboseLogs))..add(DiagnosticsProperty('showThumbnails', showThumbnails))..add(DiagnosticsProperty('bringMovedElementsToFront', bringMovedElementsToFront))..add(DiagnosticsProperty('favoriteTools', favoriteTools)); + ..add(DiagnosticsProperty('theme', theme))..add(DiagnosticsProperty('density', density))..add(DiagnosticsProperty('limitViewportMultiplier', limitViewportMultiplier))..add(DiagnosticsProperty('limitViewportPositive', limitViewportPositive))..add(DiagnosticsProperty('localeTag', localeTag))..add(DiagnosticsProperty('documentPath', documentPath))..add(DiagnosticsProperty('gestureSensitivity', gestureSensitivity))..add(DiagnosticsProperty('touchSensitivity', touchSensitivity))..add(DiagnosticsProperty('selectSensitivity', selectSensitivity))..add(DiagnosticsProperty('scrollSensitivity', scrollSensitivity))..add(DiagnosticsProperty('penOnlyInput', penOnlyInput))..add(DiagnosticsProperty('showPenOnlyToggle', showPenOnlyToggle))..add(DiagnosticsProperty('inputGestures', inputGestures))..add(DiagnosticsProperty('design', design))..add(DiagnosticsProperty('bannerVisibility', bannerVisibility))..add(DiagnosticsProperty('history', history))..add(DiagnosticsProperty('zoomEnabled', zoomEnabled))..add(DiagnosticsProperty('zoomPosition', zoomPosition))..add(DiagnosticsProperty('propertyPosition', propertyPosition))..add(DiagnosticsProperty('lastVersion', lastVersion))..add(DiagnosticsProperty('connections', connections))..add(DiagnosticsProperty('defaultRemote', defaultRemote))..add(DiagnosticsProperty('nativeTitleBar', nativeTitleBar))..add(DiagnosticsProperty('startInFullScreen', startInFullScreen))..add(DiagnosticsProperty('navigationRail', navigationRail))..add(DiagnosticsProperty('ignorePressure', ignorePressure))..add(DiagnosticsProperty('syncMode', syncMode))..add(DiagnosticsProperty('inputConfiguration', inputConfiguration))..add(DiagnosticsProperty('fallbackPack', fallbackPack))..add(DiagnosticsProperty('starred', starred))..add(DiagnosticsProperty('favoriteTemplates', favoriteTemplates))..add(DiagnosticsProperty('defaultTemplate', defaultTemplate))..add(DiagnosticsProperty('navigatorPosition', navigatorPosition))..add(DiagnosticsProperty('toolbarPosition', toolbarPosition))..add(DiagnosticsProperty('toolbarSize', toolbarSize))..add(DiagnosticsProperty('sortBy', sortBy))..add(DiagnosticsProperty('sortOrder', sortOrder))..add(DiagnosticsProperty('imageScale', imageScale))..add(DiagnosticsProperty('platformTheme', platformTheme))..add(DiagnosticsProperty('recentColors', recentColors))..add(DiagnosticsProperty('flags', flags))..add(DiagnosticsProperty('spreadPages', spreadPages))..add(DiagnosticsProperty('highContrast', highContrast))..add(DiagnosticsProperty('gridView', gridView))..add(DiagnosticsProperty('hideExtension', hideExtension))..add(DiagnosticsProperty('autosave', autosave))..add(DiagnosticsProperty('showSaveButton', showSaveButton))..add(DiagnosticsProperty('toolbarRows', toolbarRows))..add(DiagnosticsProperty('delayedAutosave', delayedAutosave))..add(DiagnosticsProperty('autosaveDelaySeconds', autosaveDelaySeconds))..add(DiagnosticsProperty('hideCursorWhileDrawing', hideCursorWhileDrawing))..add(DiagnosticsProperty('onStartup', onStartup))..add(DiagnosticsProperty('documentStatePersistence', documentStatePersistence))..add(DiagnosticsProperty('simpleToolbarVisibility', simpleToolbarVisibility))..add(DiagnosticsProperty('optionsPanelPosition', optionsPanelPosition))..add(DiagnosticsProperty('renderResolution', renderResolution))..add(DiagnosticsProperty('moveOnGesture', moveOnGesture))..add(DiagnosticsProperty('swamps', swamps))..add(DiagnosticsProperty('selectedPalette', selectedPalette))..add(DiagnosticsProperty('showVerboseLogs', showVerboseLogs))..add(DiagnosticsProperty('showThumbnails', showThumbnails))..add(DiagnosticsProperty('bringMovedElementsToFront', bringMovedElementsToFront))..add(DiagnosticsProperty('favoriteTools', favoriteTools)); } @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _ButterflySettings&&(identical(other.theme, theme) || other.theme == theme)&&(identical(other.density, density) || other.density == density)&&(identical(other.limitViewportMultiplier, limitViewportMultiplier) || other.limitViewportMultiplier == limitViewportMultiplier)&&(identical(other.limitViewportPositive, limitViewportPositive) || other.limitViewportPositive == limitViewportPositive)&&(identical(other.localeTag, localeTag) || other.localeTag == localeTag)&&(identical(other.documentPath, documentPath) || other.documentPath == documentPath)&&(identical(other.gestureSensitivity, gestureSensitivity) || other.gestureSensitivity == gestureSensitivity)&&(identical(other.touchSensitivity, touchSensitivity) || other.touchSensitivity == touchSensitivity)&&(identical(other.selectSensitivity, selectSensitivity) || other.selectSensitivity == selectSensitivity)&&(identical(other.scrollSensitivity, scrollSensitivity) || other.scrollSensitivity == scrollSensitivity)&&(identical(other.penOnlyInput, penOnlyInput) || other.penOnlyInput == penOnlyInput)&&(identical(other.showPenOnlyToggle, showPenOnlyToggle) || other.showPenOnlyToggle == showPenOnlyToggle)&&(identical(other.inputGestures, inputGestures) || other.inputGestures == inputGestures)&&(identical(other.design, design) || other.design == design)&&(identical(other.bannerVisibility, bannerVisibility) || other.bannerVisibility == bannerVisibility)&&const DeepCollectionEquality().equals(other._history, _history)&&(identical(other.zoomEnabled, zoomEnabled) || other.zoomEnabled == zoomEnabled)&&(identical(other.zoomPosition, zoomPosition) || other.zoomPosition == zoomPosition)&&(identical(other.propertyPosition, propertyPosition) || other.propertyPosition == propertyPosition)&&(identical(other.lastVersion, lastVersion) || other.lastVersion == lastVersion)&&const DeepCollectionEquality().equals(other._connections, _connections)&&(identical(other.defaultRemote, defaultRemote) || other.defaultRemote == defaultRemote)&&(identical(other.nativeTitleBar, nativeTitleBar) || other.nativeTitleBar == nativeTitleBar)&&(identical(other.startInFullScreen, startInFullScreen) || other.startInFullScreen == startInFullScreen)&&(identical(other.navigationRail, navigationRail) || other.navigationRail == navigationRail)&&(identical(other.ignorePressure, ignorePressure) || other.ignorePressure == ignorePressure)&&(identical(other.syncMode, syncMode) || other.syncMode == syncMode)&&(identical(other.inputConfiguration, inputConfiguration) || other.inputConfiguration == inputConfiguration)&&(identical(other.fallbackPack, fallbackPack) || other.fallbackPack == fallbackPack)&&const DeepCollectionEquality().equals(other._starred, _starred)&&const DeepCollectionEquality().equals(other._favoriteTemplates, _favoriteTemplates)&&(identical(other.defaultTemplate, defaultTemplate) || other.defaultTemplate == defaultTemplate)&&(identical(other.navigatorPosition, navigatorPosition) || other.navigatorPosition == navigatorPosition)&&(identical(other.toolbarPosition, toolbarPosition) || other.toolbarPosition == toolbarPosition)&&(identical(other.toolbarSize, toolbarSize) || other.toolbarSize == toolbarSize)&&(identical(other.sortBy, sortBy) || other.sortBy == sortBy)&&(identical(other.sortOrder, sortOrder) || other.sortOrder == sortOrder)&&(identical(other.imageScale, imageScale) || other.imageScale == imageScale)&&(identical(other.platformTheme, platformTheme) || other.platformTheme == platformTheme)&&const DeepCollectionEquality().equals(other._recentColors, _recentColors)&&const DeepCollectionEquality().equals(other._flags, _flags)&&(identical(other.spreadPages, spreadPages) || other.spreadPages == spreadPages)&&(identical(other.highContrast, highContrast) || other.highContrast == highContrast)&&(identical(other.gridView, gridView) || other.gridView == gridView)&&(identical(other.hideExtension, hideExtension) || other.hideExtension == hideExtension)&&(identical(other.autosave, autosave) || other.autosave == autosave)&&(identical(other.showSaveButton, showSaveButton) || other.showSaveButton == showSaveButton)&&(identical(other.toolbarRows, toolbarRows) || other.toolbarRows == toolbarRows)&&(identical(other.delayedAutosave, delayedAutosave) || other.delayedAutosave == delayedAutosave)&&(identical(other.autosaveDelaySeconds, autosaveDelaySeconds) || other.autosaveDelaySeconds == autosaveDelaySeconds)&&(identical(other.hideCursorWhileDrawing, hideCursorWhileDrawing) || other.hideCursorWhileDrawing == hideCursorWhileDrawing)&&(identical(other.onStartup, onStartup) || other.onStartup == onStartup)&&(identical(other.simpleToolbarVisibility, simpleToolbarVisibility) || other.simpleToolbarVisibility == simpleToolbarVisibility)&&(identical(other.optionsPanelPosition, optionsPanelPosition) || other.optionsPanelPosition == optionsPanelPosition)&&(identical(other.renderResolution, renderResolution) || other.renderResolution == renderResolution)&&(identical(other.moveOnGesture, moveOnGesture) || other.moveOnGesture == moveOnGesture)&&const DeepCollectionEquality().equals(other._swamps, _swamps)&&(identical(other.selectedPalette, selectedPalette) || other.selectedPalette == selectedPalette)&&(identical(other.showVerboseLogs, showVerboseLogs) || other.showVerboseLogs == showVerboseLogs)&&(identical(other.showThumbnails, showThumbnails) || other.showThumbnails == showThumbnails)&&(identical(other.bringMovedElementsToFront, bringMovedElementsToFront) || other.bringMovedElementsToFront == bringMovedElementsToFront)&&const DeepCollectionEquality().equals(other._favoriteTools, _favoriteTools)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ButterflySettings&&(identical(other.theme, theme) || other.theme == theme)&&(identical(other.density, density) || other.density == density)&&(identical(other.limitViewportMultiplier, limitViewportMultiplier) || other.limitViewportMultiplier == limitViewportMultiplier)&&(identical(other.limitViewportPositive, limitViewportPositive) || other.limitViewportPositive == limitViewportPositive)&&(identical(other.localeTag, localeTag) || other.localeTag == localeTag)&&(identical(other.documentPath, documentPath) || other.documentPath == documentPath)&&(identical(other.gestureSensitivity, gestureSensitivity) || other.gestureSensitivity == gestureSensitivity)&&(identical(other.touchSensitivity, touchSensitivity) || other.touchSensitivity == touchSensitivity)&&(identical(other.selectSensitivity, selectSensitivity) || other.selectSensitivity == selectSensitivity)&&(identical(other.scrollSensitivity, scrollSensitivity) || other.scrollSensitivity == scrollSensitivity)&&(identical(other.penOnlyInput, penOnlyInput) || other.penOnlyInput == penOnlyInput)&&(identical(other.showPenOnlyToggle, showPenOnlyToggle) || other.showPenOnlyToggle == showPenOnlyToggle)&&(identical(other.inputGestures, inputGestures) || other.inputGestures == inputGestures)&&(identical(other.design, design) || other.design == design)&&(identical(other.bannerVisibility, bannerVisibility) || other.bannerVisibility == bannerVisibility)&&const DeepCollectionEquality().equals(other._history, _history)&&(identical(other.zoomEnabled, zoomEnabled) || other.zoomEnabled == zoomEnabled)&&(identical(other.zoomPosition, zoomPosition) || other.zoomPosition == zoomPosition)&&(identical(other.propertyPosition, propertyPosition) || other.propertyPosition == propertyPosition)&&(identical(other.lastVersion, lastVersion) || other.lastVersion == lastVersion)&&const DeepCollectionEquality().equals(other._connections, _connections)&&(identical(other.defaultRemote, defaultRemote) || other.defaultRemote == defaultRemote)&&(identical(other.nativeTitleBar, nativeTitleBar) || other.nativeTitleBar == nativeTitleBar)&&(identical(other.startInFullScreen, startInFullScreen) || other.startInFullScreen == startInFullScreen)&&(identical(other.navigationRail, navigationRail) || other.navigationRail == navigationRail)&&(identical(other.ignorePressure, ignorePressure) || other.ignorePressure == ignorePressure)&&(identical(other.syncMode, syncMode) || other.syncMode == syncMode)&&(identical(other.inputConfiguration, inputConfiguration) || other.inputConfiguration == inputConfiguration)&&(identical(other.fallbackPack, fallbackPack) || other.fallbackPack == fallbackPack)&&const DeepCollectionEquality().equals(other._starred, _starred)&&const DeepCollectionEquality().equals(other._favoriteTemplates, _favoriteTemplates)&&(identical(other.defaultTemplate, defaultTemplate) || other.defaultTemplate == defaultTemplate)&&(identical(other.navigatorPosition, navigatorPosition) || other.navigatorPosition == navigatorPosition)&&(identical(other.toolbarPosition, toolbarPosition) || other.toolbarPosition == toolbarPosition)&&(identical(other.toolbarSize, toolbarSize) || other.toolbarSize == toolbarSize)&&(identical(other.sortBy, sortBy) || other.sortBy == sortBy)&&(identical(other.sortOrder, sortOrder) || other.sortOrder == sortOrder)&&(identical(other.imageScale, imageScale) || other.imageScale == imageScale)&&(identical(other.platformTheme, platformTheme) || other.platformTheme == platformTheme)&&const DeepCollectionEquality().equals(other._recentColors, _recentColors)&&const DeepCollectionEquality().equals(other._flags, _flags)&&(identical(other.spreadPages, spreadPages) || other.spreadPages == spreadPages)&&(identical(other.highContrast, highContrast) || other.highContrast == highContrast)&&(identical(other.gridView, gridView) || other.gridView == gridView)&&(identical(other.hideExtension, hideExtension) || other.hideExtension == hideExtension)&&(identical(other.autosave, autosave) || other.autosave == autosave)&&(identical(other.showSaveButton, showSaveButton) || other.showSaveButton == showSaveButton)&&(identical(other.toolbarRows, toolbarRows) || other.toolbarRows == toolbarRows)&&(identical(other.delayedAutosave, delayedAutosave) || other.delayedAutosave == delayedAutosave)&&(identical(other.autosaveDelaySeconds, autosaveDelaySeconds) || other.autosaveDelaySeconds == autosaveDelaySeconds)&&(identical(other.hideCursorWhileDrawing, hideCursorWhileDrawing) || other.hideCursorWhileDrawing == hideCursorWhileDrawing)&&(identical(other.onStartup, onStartup) || other.onStartup == onStartup)&&(identical(other.documentStatePersistence, documentStatePersistence) || other.documentStatePersistence == documentStatePersistence)&&(identical(other.simpleToolbarVisibility, simpleToolbarVisibility) || other.simpleToolbarVisibility == simpleToolbarVisibility)&&(identical(other.optionsPanelPosition, optionsPanelPosition) || other.optionsPanelPosition == optionsPanelPosition)&&(identical(other.renderResolution, renderResolution) || other.renderResolution == renderResolution)&&(identical(other.moveOnGesture, moveOnGesture) || other.moveOnGesture == moveOnGesture)&&const DeepCollectionEquality().equals(other._swamps, _swamps)&&(identical(other.selectedPalette, selectedPalette) || other.selectedPalette == selectedPalette)&&(identical(other.showVerboseLogs, showVerboseLogs) || other.showVerboseLogs == showVerboseLogs)&&(identical(other.showThumbnails, showThumbnails) || other.showThumbnails == showThumbnails)&&(identical(other.bringMovedElementsToFront, bringMovedElementsToFront) || other.bringMovedElementsToFront == bringMovedElementsToFront)&&const DeepCollectionEquality().equals(other._favoriteTools, _favoriteTools)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hashAll([runtimeType,theme,density,limitViewportMultiplier,limitViewportPositive,localeTag,documentPath,gestureSensitivity,touchSensitivity,selectSensitivity,scrollSensitivity,penOnlyInput,showPenOnlyToggle,inputGestures,design,bannerVisibility,const DeepCollectionEquality().hash(_history),zoomEnabled,zoomPosition,propertyPosition,lastVersion,const DeepCollectionEquality().hash(_connections),defaultRemote,nativeTitleBar,startInFullScreen,navigationRail,ignorePressure,syncMode,inputConfiguration,fallbackPack,const DeepCollectionEquality().hash(_starred),const DeepCollectionEquality().hash(_favoriteTemplates),defaultTemplate,navigatorPosition,toolbarPosition,toolbarSize,sortBy,sortOrder,imageScale,platformTheme,const DeepCollectionEquality().hash(_recentColors),const DeepCollectionEquality().hash(_flags),spreadPages,highContrast,gridView,hideExtension,autosave,showSaveButton,toolbarRows,delayedAutosave,autosaveDelaySeconds,hideCursorWhileDrawing,onStartup,simpleToolbarVisibility,optionsPanelPosition,renderResolution,moveOnGesture,const DeepCollectionEquality().hash(_swamps),selectedPalette,showVerboseLogs,showThumbnails,bringMovedElementsToFront,const DeepCollectionEquality().hash(_favoriteTools)]); +int get hashCode => Object.hashAll([runtimeType,theme,density,limitViewportMultiplier,limitViewportPositive,localeTag,documentPath,gestureSensitivity,touchSensitivity,selectSensitivity,scrollSensitivity,penOnlyInput,showPenOnlyToggle,inputGestures,design,bannerVisibility,const DeepCollectionEquality().hash(_history),zoomEnabled,zoomPosition,propertyPosition,lastVersion,const DeepCollectionEquality().hash(_connections),defaultRemote,nativeTitleBar,startInFullScreen,navigationRail,ignorePressure,syncMode,inputConfiguration,fallbackPack,const DeepCollectionEquality().hash(_starred),const DeepCollectionEquality().hash(_favoriteTemplates),defaultTemplate,navigatorPosition,toolbarPosition,toolbarSize,sortBy,sortOrder,imageScale,platformTheme,const DeepCollectionEquality().hash(_recentColors),const DeepCollectionEquality().hash(_flags),spreadPages,highContrast,gridView,hideExtension,autosave,showSaveButton,toolbarRows,delayedAutosave,autosaveDelaySeconds,hideCursorWhileDrawing,onStartup,documentStatePersistence,simpleToolbarVisibility,optionsPanelPosition,renderResolution,moveOnGesture,const DeepCollectionEquality().hash(_swamps),selectedPalette,showVerboseLogs,showThumbnails,bringMovedElementsToFront,const DeepCollectionEquality().hash(_favoriteTools)]); @override String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'ButterflySettings(theme: $theme, density: $density, limitViewportMultiplier: $limitViewportMultiplier, limitViewportPositive: $limitViewportPositive, localeTag: $localeTag, documentPath: $documentPath, gestureSensitivity: $gestureSensitivity, touchSensitivity: $touchSensitivity, selectSensitivity: $selectSensitivity, scrollSensitivity: $scrollSensitivity, penOnlyInput: $penOnlyInput, showPenOnlyToggle: $showPenOnlyToggle, inputGestures: $inputGestures, design: $design, bannerVisibility: $bannerVisibility, history: $history, zoomEnabled: $zoomEnabled, zoomPosition: $zoomPosition, propertyPosition: $propertyPosition, lastVersion: $lastVersion, connections: $connections, defaultRemote: $defaultRemote, nativeTitleBar: $nativeTitleBar, startInFullScreen: $startInFullScreen, navigationRail: $navigationRail, ignorePressure: $ignorePressure, syncMode: $syncMode, inputConfiguration: $inputConfiguration, fallbackPack: $fallbackPack, starred: $starred, favoriteTemplates: $favoriteTemplates, defaultTemplate: $defaultTemplate, navigatorPosition: $navigatorPosition, toolbarPosition: $toolbarPosition, toolbarSize: $toolbarSize, sortBy: $sortBy, sortOrder: $sortOrder, imageScale: $imageScale, platformTheme: $platformTheme, recentColors: $recentColors, flags: $flags, spreadPages: $spreadPages, highContrast: $highContrast, gridView: $gridView, hideExtension: $hideExtension, autosave: $autosave, showSaveButton: $showSaveButton, toolbarRows: $toolbarRows, delayedAutosave: $delayedAutosave, autosaveDelaySeconds: $autosaveDelaySeconds, hideCursorWhileDrawing: $hideCursorWhileDrawing, onStartup: $onStartup, simpleToolbarVisibility: $simpleToolbarVisibility, optionsPanelPosition: $optionsPanelPosition, renderResolution: $renderResolution, moveOnGesture: $moveOnGesture, swamps: $swamps, selectedPalette: $selectedPalette, showVerboseLogs: $showVerboseLogs, showThumbnails: $showThumbnails, bringMovedElementsToFront: $bringMovedElementsToFront, favoriteTools: $favoriteTools)'; + return 'ButterflySettings(theme: $theme, density: $density, limitViewportMultiplier: $limitViewportMultiplier, limitViewportPositive: $limitViewportPositive, localeTag: $localeTag, documentPath: $documentPath, gestureSensitivity: $gestureSensitivity, touchSensitivity: $touchSensitivity, selectSensitivity: $selectSensitivity, scrollSensitivity: $scrollSensitivity, penOnlyInput: $penOnlyInput, showPenOnlyToggle: $showPenOnlyToggle, inputGestures: $inputGestures, design: $design, bannerVisibility: $bannerVisibility, history: $history, zoomEnabled: $zoomEnabled, zoomPosition: $zoomPosition, propertyPosition: $propertyPosition, lastVersion: $lastVersion, connections: $connections, defaultRemote: $defaultRemote, nativeTitleBar: $nativeTitleBar, startInFullScreen: $startInFullScreen, navigationRail: $navigationRail, ignorePressure: $ignorePressure, syncMode: $syncMode, inputConfiguration: $inputConfiguration, fallbackPack: $fallbackPack, starred: $starred, favoriteTemplates: $favoriteTemplates, defaultTemplate: $defaultTemplate, navigatorPosition: $navigatorPosition, toolbarPosition: $toolbarPosition, toolbarSize: $toolbarSize, sortBy: $sortBy, sortOrder: $sortOrder, imageScale: $imageScale, platformTheme: $platformTheme, recentColors: $recentColors, flags: $flags, spreadPages: $spreadPages, highContrast: $highContrast, gridView: $gridView, hideExtension: $hideExtension, autosave: $autosave, showSaveButton: $showSaveButton, toolbarRows: $toolbarRows, delayedAutosave: $delayedAutosave, autosaveDelaySeconds: $autosaveDelaySeconds, hideCursorWhileDrawing: $hideCursorWhileDrawing, onStartup: $onStartup, documentStatePersistence: $documentStatePersistence, simpleToolbarVisibility: $simpleToolbarVisibility, optionsPanelPosition: $optionsPanelPosition, renderResolution: $renderResolution, moveOnGesture: $moveOnGesture, swamps: $swamps, selectedPalette: $selectedPalette, showVerboseLogs: $showVerboseLogs, showThumbnails: $showThumbnails, bringMovedElementsToFront: $bringMovedElementsToFront, favoriteTools: $favoriteTools)'; } @@ -843,11 +1027,11 @@ abstract mixin class _$ButterflySettingsCopyWith<$Res> implements $ButterflySett factory _$ButterflySettingsCopyWith(_ButterflySettings value, $Res Function(_ButterflySettings) _then) = __$ButterflySettingsCopyWithImpl; @override @useResult $Res call({ - ThemeMode theme, ThemeDensity density, double? limitViewportMultiplier, bool limitViewportPositive, String localeTag, String documentPath, double gestureSensitivity, double touchSensitivity, double selectSensitivity, double scrollSensitivity, bool? penOnlyInput, bool showPenOnlyToggle, bool inputGestures, String design, BannerVisibility bannerVisibility,@JsonKey(includeFromJson: false, includeToJson: false) List history, bool zoomEnabled, ZoomPosition zoomPosition, ZoomPosition propertyPosition, String? lastVersion,@JsonKey(includeFromJson: false, includeToJson: false) List connections, String defaultRemote, bool nativeTitleBar, bool startInFullScreen, bool navigationRail, IgnorePressure ignorePressure, SyncMode syncMode, InputConfiguration inputConfiguration, String fallbackPack, List starred, List favoriteTemplates, String defaultTemplate, NavigatorPosition navigatorPosition, ToolbarPosition toolbarPosition, ToolbarSize toolbarSize, SortBy sortBy, SortOrder sortOrder, double imageScale, PlatformTheme platformTheme,@SRGBConverter() List recentColors, List flags, bool spreadPages, bool highContrast, bool gridView, bool hideExtension, bool autosave, bool showSaveButton, int toolbarRows, bool delayedAutosave, int autosaveDelaySeconds, bool hideCursorWhileDrawing, StartupBehavior onStartup, SimpleToolbarVisibility simpleToolbarVisibility, OptionsPanelPosition optionsPanelPosition, RenderResolution renderResolution, bool moveOnGesture, List swamps, PackAssetLocation? selectedPalette, bool showVerboseLogs, bool showThumbnails, bool bringMovedElementsToFront, List favoriteTools + ThemeMode theme, ThemeDensity density, double? limitViewportMultiplier, bool limitViewportPositive, String localeTag, String documentPath, double gestureSensitivity, double touchSensitivity, double selectSensitivity, double scrollSensitivity, bool? penOnlyInput, bool showPenOnlyToggle, bool inputGestures, String design, BannerVisibility bannerVisibility,@JsonKey(includeFromJson: false, includeToJson: false) List history, bool zoomEnabled, ZoomPosition zoomPosition, ZoomPosition propertyPosition, String? lastVersion,@JsonKey(includeFromJson: false, includeToJson: false) List connections, String defaultRemote, bool nativeTitleBar, bool startInFullScreen, bool navigationRail, IgnorePressure ignorePressure, SyncMode syncMode, InputConfiguration inputConfiguration, String fallbackPack, List starred, List favoriteTemplates, String defaultTemplate, NavigatorPosition navigatorPosition, ToolbarPosition toolbarPosition, ToolbarSize toolbarSize, SortBy sortBy, SortOrder sortOrder, double imageScale, PlatformTheme platformTheme,@SRGBConverter() List recentColors, List flags, bool spreadPages, bool highContrast, bool gridView, bool hideExtension, bool autosave, bool showSaveButton, int toolbarRows, bool delayedAutosave, int autosaveDelaySeconds, bool hideCursorWhileDrawing, StartupBehavior onStartup, DocumentStatePersistenceSettings documentStatePersistence, SimpleToolbarVisibility simpleToolbarVisibility, OptionsPanelPosition optionsPanelPosition, RenderResolution renderResolution, bool moveOnGesture, List swamps, PackAssetLocation? selectedPalette, bool showVerboseLogs, bool showThumbnails, bool bringMovedElementsToFront, List favoriteTools }); -@override $InputConfigurationCopyWith<$Res> get inputConfiguration;@override $PackAssetLocationCopyWith<$Res>? get selectedPalette; +@override $InputConfigurationCopyWith<$Res> get inputConfiguration;@override $DocumentStatePersistenceSettingsCopyWith<$Res> get documentStatePersistence;@override $PackAssetLocationCopyWith<$Res>? get selectedPalette; } /// @nodoc @@ -860,7 +1044,7 @@ class __$ButterflySettingsCopyWithImpl<$Res> /// Create a copy of ButterflySettings /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? theme = null,Object? density = null,Object? limitViewportMultiplier = freezed,Object? limitViewportPositive = null,Object? localeTag = null,Object? documentPath = null,Object? gestureSensitivity = null,Object? touchSensitivity = null,Object? selectSensitivity = null,Object? scrollSensitivity = null,Object? penOnlyInput = freezed,Object? showPenOnlyToggle = null,Object? inputGestures = null,Object? design = null,Object? bannerVisibility = null,Object? history = null,Object? zoomEnabled = null,Object? zoomPosition = null,Object? propertyPosition = null,Object? lastVersion = freezed,Object? connections = null,Object? defaultRemote = null,Object? nativeTitleBar = null,Object? startInFullScreen = null,Object? navigationRail = null,Object? ignorePressure = null,Object? syncMode = null,Object? inputConfiguration = null,Object? fallbackPack = null,Object? starred = null,Object? favoriteTemplates = null,Object? defaultTemplate = null,Object? navigatorPosition = null,Object? toolbarPosition = null,Object? toolbarSize = null,Object? sortBy = null,Object? sortOrder = null,Object? imageScale = null,Object? platformTheme = null,Object? recentColors = null,Object? flags = null,Object? spreadPages = null,Object? highContrast = null,Object? gridView = null,Object? hideExtension = null,Object? autosave = null,Object? showSaveButton = null,Object? toolbarRows = null,Object? delayedAutosave = null,Object? autosaveDelaySeconds = null,Object? hideCursorWhileDrawing = null,Object? onStartup = null,Object? simpleToolbarVisibility = null,Object? optionsPanelPosition = null,Object? renderResolution = null,Object? moveOnGesture = null,Object? swamps = null,Object? selectedPalette = freezed,Object? showVerboseLogs = null,Object? showThumbnails = null,Object? bringMovedElementsToFront = null,Object? favoriteTools = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? theme = null,Object? density = null,Object? limitViewportMultiplier = freezed,Object? limitViewportPositive = null,Object? localeTag = null,Object? documentPath = null,Object? gestureSensitivity = null,Object? touchSensitivity = null,Object? selectSensitivity = null,Object? scrollSensitivity = null,Object? penOnlyInput = freezed,Object? showPenOnlyToggle = null,Object? inputGestures = null,Object? design = null,Object? bannerVisibility = null,Object? history = null,Object? zoomEnabled = null,Object? zoomPosition = null,Object? propertyPosition = null,Object? lastVersion = freezed,Object? connections = null,Object? defaultRemote = null,Object? nativeTitleBar = null,Object? startInFullScreen = null,Object? navigationRail = null,Object? ignorePressure = null,Object? syncMode = null,Object? inputConfiguration = null,Object? fallbackPack = null,Object? starred = null,Object? favoriteTemplates = null,Object? defaultTemplate = null,Object? navigatorPosition = null,Object? toolbarPosition = null,Object? toolbarSize = null,Object? sortBy = null,Object? sortOrder = null,Object? imageScale = null,Object? platformTheme = null,Object? recentColors = null,Object? flags = null,Object? spreadPages = null,Object? highContrast = null,Object? gridView = null,Object? hideExtension = null,Object? autosave = null,Object? showSaveButton = null,Object? toolbarRows = null,Object? delayedAutosave = null,Object? autosaveDelaySeconds = null,Object? hideCursorWhileDrawing = null,Object? onStartup = null,Object? documentStatePersistence = null,Object? simpleToolbarVisibility = null,Object? optionsPanelPosition = null,Object? renderResolution = null,Object? moveOnGesture = null,Object? swamps = null,Object? selectedPalette = freezed,Object? showVerboseLogs = null,Object? showThumbnails = null,Object? bringMovedElementsToFront = null,Object? favoriteTools = null,}) { return _then(_ButterflySettings( theme: null == theme ? _self.theme : theme // ignore: cast_nullable_to_non_nullable as ThemeMode,density: null == density ? _self.density : density // ignore: cast_nullable_to_non_nullable @@ -914,7 +1098,8 @@ as int,delayedAutosave: null == delayedAutosave ? _self.delayedAutosave : delaye as bool,autosaveDelaySeconds: null == autosaveDelaySeconds ? _self.autosaveDelaySeconds : autosaveDelaySeconds // ignore: cast_nullable_to_non_nullable as int,hideCursorWhileDrawing: null == hideCursorWhileDrawing ? _self.hideCursorWhileDrawing : hideCursorWhileDrawing // ignore: cast_nullable_to_non_nullable as bool,onStartup: null == onStartup ? _self.onStartup : onStartup // ignore: cast_nullable_to_non_nullable -as StartupBehavior,simpleToolbarVisibility: null == simpleToolbarVisibility ? _self.simpleToolbarVisibility : simpleToolbarVisibility // ignore: cast_nullable_to_non_nullable +as StartupBehavior,documentStatePersistence: null == documentStatePersistence ? _self.documentStatePersistence : documentStatePersistence // ignore: cast_nullable_to_non_nullable +as DocumentStatePersistenceSettings,simpleToolbarVisibility: null == simpleToolbarVisibility ? _self.simpleToolbarVisibility : simpleToolbarVisibility // ignore: cast_nullable_to_non_nullable as SimpleToolbarVisibility,optionsPanelPosition: null == optionsPanelPosition ? _self.optionsPanelPosition : optionsPanelPosition // ignore: cast_nullable_to_non_nullable as OptionsPanelPosition,renderResolution: null == renderResolution ? _self.renderResolution : renderResolution // ignore: cast_nullable_to_non_nullable as RenderResolution,moveOnGesture: null == moveOnGesture ? _self.moveOnGesture : moveOnGesture // ignore: cast_nullable_to_non_nullable @@ -941,6 +1126,15 @@ $InputConfigurationCopyWith<$Res> get inputConfiguration { /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') +$DocumentStatePersistenceSettingsCopyWith<$Res> get documentStatePersistence { + + return $DocumentStatePersistenceSettingsCopyWith<$Res>(_self.documentStatePersistence, (value) { + return _then(_self.copyWith(documentStatePersistence: value)); + }); +}/// Create a copy of ButterflySettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') $PackAssetLocationCopyWith<$Res>? get selectedPalette { if (_self.selectedPalette == null) { return null; diff --git a/app/lib/cubits/settings.g.dart b/app/lib/cubits/settings.g.dart index 1f584c24cb61..aee81700007d 100644 --- a/app/lib/cubits/settings.g.dart +++ b/app/lib/cubits/settings.g.dart @@ -108,6 +108,36 @@ Map _$InputConfigurationToJson(_InputConfiguration instance) => 'tripleTouchShortcut': instance.tripleTouchShortcut, }; +_DocumentStatePersistenceSettings _$DocumentStatePersistenceSettingsFromJson( + Map json, +) => _DocumentStatePersistenceSettings( + enabled: json['enabled'] as bool? ?? true, + page: json['page'] as bool? ?? true, + camera: json['camera'] as bool? ?? true, + locks: json['locks'] as bool? ?? true, + tool: json['tool'] as bool? ?? true, + navigator: json['navigator'] as bool? ?? true, + layers: json['layers'] as bool? ?? true, + areas: json['areas'] as bool? ?? true, + maxEntries: (json['maxEntries'] as num?)?.toInt() ?? 400, + maxAgeDays: (json['maxAgeDays'] as num?)?.toInt() ?? 180, +); + +Map _$DocumentStatePersistenceSettingsToJson( + _DocumentStatePersistenceSettings instance, +) => { + 'enabled': instance.enabled, + 'page': instance.page, + 'camera': instance.camera, + 'locks': instance.locks, + 'tool': instance.tool, + 'navigator': instance.navigator, + 'layers': instance.layers, + 'areas': instance.areas, + 'maxEntries': instance.maxEntries, + 'maxAgeDays': instance.maxAgeDays, +}; + _ButterflySettings _$ButterflySettingsFromJson(Map json) => _ButterflySettings( theme: $enumDecodeNullable(_$ThemeModeEnumMap, json['theme']) ?? @@ -212,6 +242,11 @@ _ButterflySettings _$ButterflySettingsFromJson(Map json) => _ButterflySettings( onStartup: $enumDecodeNullable(_$StartupBehaviorEnumMap, json['onStartup']) ?? StartupBehavior.openHomeScreen, + documentStatePersistence: json['documentStatePersistence'] == null + ? const DocumentStatePersistenceSettings() + : DocumentStatePersistenceSettings.fromJson( + Map.from(json['documentStatePersistence'] as Map), + ), simpleToolbarVisibility: $enumDecodeNullable( _$SimpleToolbarVisibilityEnumMap, @@ -310,6 +345,7 @@ Map _$ButterflySettingsToJson( 'autosaveDelaySeconds': instance.autosaveDelaySeconds, 'hideCursorWhileDrawing': instance.hideCursorWhileDrawing, 'onStartup': _$StartupBehaviorEnumMap[instance.onStartup]!, + 'documentStatePersistence': instance.documentStatePersistence.toJson(), 'simpleToolbarVisibility': _$SimpleToolbarVisibilityEnumMap[instance.simpleToolbarVisibility]!, 'optionsPanelPosition': diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 02ed11cb0a87..b88a176cb1a2 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -1371,5 +1371,6 @@ "bringMovedElementsToFront": "Bring moved elements to front", "addTool": "Add tool", "nextPage": "Next page", - "previousPage": "Previous page" + "previousPage": "Previous page", + "persistenceDocumentStates": "Persistent document states" } diff --git a/app/lib/main.dart b/app/lib/main.dart index 077d2ffe0c88..8b9c3c681f15 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -5,7 +5,8 @@ import 'package:butterfly/api/close.dart'; import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/api/intent.dart'; import 'package:butterfly/services/sync.dart'; -import 'package:butterfly/settings/behaviors.dart'; +import 'package:butterfly/settings/behaviors/home.dart'; +import 'package:butterfly/settings/behaviors/persistence.dart'; import 'package:butterfly/settings/inputs/mouse.dart'; import 'package:butterfly/settings/experiments.dart'; import 'package:butterfly/settings/view.dart'; @@ -191,6 +192,13 @@ class ButterflyApp extends StatelessWidget { GoRoute( path: 'behaviors', builder: (context, state) => const BehaviorsSettingsPage(), + routes: [ + GoRoute( + path: 'persistence', + builder: (context, state) => + const PersistenceBehaviorSettings(), + ), + ], ), GoRoute( path: 'personalization', diff --git a/app/lib/repositories/document_state.dart b/app/lib/repositories/document_state.dart index dc291856a3fd..b0f3d5ec5cbb 100644 --- a/app/lib/repositories/document_state.dart +++ b/app/lib/repositories/document_state.dart @@ -1,25 +1,34 @@ import 'package:butterfly/api/file_system.dart'; +import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/models/persisted_document_state.dart'; class DocumentStateRepository { - DocumentStateRepository(this.fileSystem); + DocumentStateRepository(this.fileSystem, {this.settingsProvider}); final DocumentStateFileSystem fileSystem; + final DocumentStatePersistenceSettings Function()? settingsProvider; + var _cleanupStarted = false; + + DocumentStatePersistenceSettings get _settings => + settingsProvider?.call() ?? const DocumentStatePersistenceSettings(); Future load({ String? contentHash, String? pathKey, bool allowContentHash = true, }) async { + final settings = _settings; + if (!settings.enabled) return null; await fileSystem.initialize(); if (allowContentHash && contentHash != null) { final byContent = await fileSystem.getFile( documentStateContentKey(contentHash), ); - if (byContent != null) return byContent; + if (byContent != null) return _applySettings(byContent, settings); } if (pathKey != null) { - return fileSystem.getFile(pathKey); + final byPath = await fileSystem.getFile(pathKey); + return byPath == null ? null : _applySettings(byPath, settings); } return null; } @@ -29,20 +38,130 @@ class DocumentStateRepository { String? contentHash, String? pathKey, }) async { + final settings = _settings; + if (!settings.enabled) return; await fileSystem.initialize(); if (contentHash != null) { - await _put(documentStateContentKey(contentHash), state); + await _put(documentStateContentKey(contentHash), state, settings); } if (pathKey != null) { - await _put(pathKey, state); + await _put(pathKey, state, settings); } + await _cleanupAfterSave(contentHash: contentHash, pathKey: pathKey); } - Future _put(String key, PersistedDocumentState state) async { - if (await fileSystem.hasKey(key)) { - await fileSystem.updateFile(key, state); + Future cleanup({ + String? contentHash, + String? pathKey, + DateTime? now, + }) async { + final settings = _settings; + await fileSystem.initialize(); + final keys = await fileSystem.getKeys(); + final protectedKeys = {}; + if (contentHash != null) { + protectedKeys.add(documentStateContentKey(contentHash)); + } + if (pathKey != null) { + protectedKeys.add(pathKey); + } + final records = <({String key, PersistedDocumentState state})>[]; + for (final key in keys) { + final state = await fileSystem.getFile(key); + if (state == null) continue; + records.add((key: key, state: state)); + } + final cutoff = (now ?? DateTime.now().toUtc()).subtract( + Duration(days: settings.maxAgeDays), + ); + for (final record in records) { + final updatedAt = record.state.updatedAt; + if (protectedKeys.contains(record.key) || updatedAt == null) continue; + if (updatedAt.isBefore(cutoff)) { + await fileSystem.deleteFile(record.key); + } + } + final remaining = <({String key, PersistedDocumentState state})>[]; + for (final key in await fileSystem.getKeys()) { + final state = await fileSystem.getFile(key); + if (state != null) remaining.add((key: key, state: state)); + } + remaining.sort((a, b) { + final aTime = a.state.updatedAt ?? DateTime.fromMillisecondsSinceEpoch(0); + final bTime = b.state.updatedAt ?? DateTime.fromMillisecondsSinceEpoch(0); + return aTime.compareTo(bTime); + }); + final removable = remaining + .where((record) => !protectedKeys.contains(record.key)) + .toList(); + final overflow = (remaining.length - settings.maxEntries).clamp( + 0, + removable.length, + ); + for (final record in removable.take(overflow)) { + await fileSystem.deleteFile(record.key); + } + } + + Future _put( + String key, + PersistedDocumentState state, + DocumentStatePersistenceSettings settings, + ) async { + final existing = await fileSystem.getFile(key); + final next = _mergeEnabled(existing, state, settings); + if (existing != null || await fileSystem.hasKey(key)) { + await fileSystem.updateFile(key, next); } else { - await fileSystem.createFile(key, state); + await fileSystem.createFile(key, next); + } + } + + PersistedDocumentState _applySettings( + PersistedDocumentState state, + DocumentStatePersistenceSettings settings, + ) => state.copyWith( + pageName: settings.page ? state.pageName : null, + camera: settings.camera ? state.camera : const PersistedCameraState(), + locks: settings.locks ? state.locks : const PersistentLockState(), + selectedTool: settings.tool + ? state.selectedTool + : const PersistedToolSelection(), + navigator: settings.navigator + ? state.navigator + : const PersistedNavigatorState(), + layers: settings.layers ? state.layers : const PersistedLayerState(), + areaNavigator: settings.areas + ? state.areaNavigator + : const PersistedAreaNavigatorState(), + ); + + PersistedDocumentState _mergeEnabled( + PersistedDocumentState? existing, + PersistedDocumentState state, + DocumentStatePersistenceSettings settings, + ) { + final fallback = existing ?? const PersistedDocumentState(); + return state.copyWith( + pageName: settings.page ? state.pageName : fallback.pageName, + camera: settings.camera ? state.camera : fallback.camera, + locks: settings.locks ? state.locks : fallback.locks, + selectedTool: settings.tool ? state.selectedTool : fallback.selectedTool, + navigator: settings.navigator ? state.navigator : fallback.navigator, + layers: settings.layers ? state.layers : fallback.layers, + areaNavigator: settings.areas + ? state.areaNavigator + : fallback.areaNavigator, + ); + } + + Future _cleanupAfterSave({String? contentHash, String? pathKey}) async { + if (_cleanupStarted) return; + _cleanupStarted = true; + try { + await cleanup(contentHash: contentHash, pathKey: pathKey); + } finally { + _cleanupStarted = false; } } } diff --git a/app/lib/settings/behaviors.dart b/app/lib/settings/behaviors/home.dart similarity index 94% rename from app/lib/settings/behaviors.dart rename to app/lib/settings/behaviors/home.dart index 7510e2c8b693..3b412519654f 100644 --- a/app/lib/settings/behaviors.dart +++ b/app/lib/settings/behaviors/home.dart @@ -3,6 +3,7 @@ import 'package:butterfly/theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; +import 'package:go_router/go_router.dart'; import 'package:material_leap/material_leap.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; @@ -101,6 +102,23 @@ class BehaviorsSettingsPage extends StatelessWidget { onTap: () => _openStartupModal(context), leading: const Icon(PhosphorIconsLight.arrowFatLineUp), ), + ListTile( + title: Text( + AppLocalizations.of( + context, + ).persistenceDocumentStates, + ), + subtitle: Text( + state.documentStatePersistence.enabled + ? 'Enabled' + : AppLocalizations.of(context).off, + ), + onTap: () => + context.push('/settings/behaviors/persistence'), + leading: const PhosphorIcon( + PhosphorIconsLight.database, + ), + ), SwitchListTile( value: state.startInFullScreen, onChanged: (value) => context diff --git a/app/lib/settings/behaviors/persistence.dart b/app/lib/settings/behaviors/persistence.dart new file mode 100644 index 000000000000..abdd3fd9f1ea --- /dev/null +++ b/app/lib/settings/behaviors/persistence.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:butterfly/src/generated/i18n/app_localizations.dart'; +import 'package:material_leap/material_leap.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +import '../../cubits/settings.dart'; + +class PersistenceBehaviorSettings extends StatelessWidget { + const PersistenceBehaviorSettings({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: WindowTitleBar( + title: Text(AppLocalizations.of(context).persistenceDocumentStates), + ), + body: Align( + alignment: Alignment.center, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: LeapBreakpoints.compact), + child: BlocBuilder( + builder: (context, state) { + final settings = state.documentStatePersistence; + void change(DocumentStatePersistenceSettings next) { + context.read().changeDocumentStatePersistence( + next, + ); + } + + return ListView( + children: [ + SwitchListTile( + value: settings.enabled, + secondary: const PhosphorIcon(PhosphorIconsLight.power), + title: const Text('Enable persistent document states'), + onChanged: (value) => + change(settings.copyWith(enabled: value)), + ), + const Divider(), + SwitchListTile( + value: settings.page, + secondary: const PhosphorIcon(PhosphorIconsLight.file), + title: const Text('Current page'), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(page: value)) + : null, + ), + SwitchListTile( + value: settings.camera, + secondary: const PhosphorIcon( + PhosphorIconsLight.frameCorners, + ), + title: const Text('Viewport position and zoom'), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(camera: value)) + : null, + ), + SwitchListTile( + value: settings.locks, + secondary: const PhosphorIcon(PhosphorIconsLight.lockKey), + title: Text(AppLocalizations.of(context).lock), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(locks: value)) + : null, + ), + SwitchListTile( + value: settings.tool, + secondary: const PhosphorIcon(PhosphorIconsLight.toolbox), + title: const Text('Selected tool'), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(tool: value)) + : null, + ), + SwitchListTile( + value: settings.navigator, + secondary: const PhosphorIcon(PhosphorIconsLight.sidebar), + title: Text(AppLocalizations.of(context).navigator), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(navigator: value)) + : null, + ), + SwitchListTile( + value: settings.layers, + secondary: const PhosphorIcon(PhosphorIconsLight.stack), + title: Text(AppLocalizations.of(context).layers), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(layers: value)) + : null, + ), + SwitchListTile( + value: settings.areas, + secondary: const PhosphorIcon(PhosphorIconsLight.selection), + title: Text(AppLocalizations.of(context).areas), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(areas: value)) + : null, + ), + const Divider(), + ExactSlider( + header: const Text('Maximum stored records'), + leading: const PhosphorIcon(PhosphorIconsLight.listNumbers), + value: settings.maxEntries.toDouble(), + min: 20, + max: 2000, + defaultValue: 400, + fractionDigits: 0, + onChangeEnd: (value) => + change(settings.copyWith(maxEntries: value.toInt())), + ), + ExactSlider( + header: const Text('Delete records older than days'), + leading: const PhosphorIcon(PhosphorIconsLight.calendar), + value: settings.maxAgeDays.toDouble(), + min: 7, + max: 730, + defaultValue: 180, + fractionDigits: 0, + onChangeEnd: (value) => + change(settings.copyWith(maxAgeDays: value.toInt())), + ), + ], + ); + }, + ), + ), + ), + ); + } +} diff --git a/app/lib/settings/home.dart b/app/lib/settings/home.dart index 12ba6e8b08d9..d395514078e6 100644 --- a/app/lib/settings/home.dart +++ b/app/lib/settings/home.dart @@ -1,4 +1,4 @@ -import 'package:butterfly/settings/behaviors.dart'; +import 'package:butterfly/settings/behaviors/home.dart'; import 'package:butterfly/settings/inputs/home.dart'; import 'package:butterfly/settings/data.dart'; import 'package:butterfly/settings/personalization.dart'; diff --git a/app/lib/views/main.dart b/app/lib/views/main.dart index a176a9539d2c..7f83e3b51441 100644 --- a/app/lib/views/main.dart +++ b/app/lib/views/main.dart @@ -343,6 +343,7 @@ class _ProjectPageState extends State { : documentStateContentHash(loadedDocumentBytes); final documentStateRepository = DocumentStateRepository( fileSystem.buildDocumentStateSystem(remote), + settingsProvider: () => settingsCubit.state.documentStatePersistence, ); final restoredSession = await documentStateRepository.load( contentHash: contentHash, diff --git a/app/test/cubits/editor_session_test.dart b/app/test/cubits/editor_session_test.dart index e0194a29f721..10509492ee9f 100644 --- a/app/test/cubits/editor_session_test.dart +++ b/app/test/cubits/editor_session_test.dart @@ -1,5 +1,6 @@ import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/cubits/editor_session.dart'; +import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/models/persisted_document_state.dart'; import 'package:butterfly/repositories/document_state.dart'; @@ -99,6 +100,104 @@ void main() { expect(loaded, isNull); }); + test('does not load or save when persistence is disabled', () async { + const state = PersistedDocumentState(pageName: 'Page 1'); + await fileSystem.initialize(); + await fileSystem.createFile('path/a', state); + + final repository = DocumentStateRepository( + fileSystem, + settingsProvider: () => + const DocumentStatePersistenceSettings(enabled: false), + ); + + expect(await repository.load(pathKey: 'path/a'), isNull); + await repository.save( + const PersistedDocumentState(pageName: 'Changed'), + pathKey: 'path/b', + ); + expect(await fileSystem.getFile('path/b'), isNull); + }); + + test('filters disabled categories on load', () async { + final state = PersistedDocumentState( + pageName: 'Page 1', + locks: const PersistentLockState(lockZoom: true), + areaNavigator: const PersistedAreaNavigatorState(create: false), + ); + await fileSystem.initialize(); + await fileSystem.createFile('path/a', state); + + final loaded = await DocumentStateRepository( + fileSystem, + settingsProvider: () => + const DocumentStatePersistenceSettings(locks: false, areas: false), + ).load(pathKey: 'path/a'); + + expect(loaded?.pageName, 'Page 1'); + expect(loaded?.locks, const PersistentLockState()); + expect(loaded?.areaNavigator, const PersistedAreaNavigatorState()); + }); + + test('preserves disabled categories on save', () async { + final existing = PersistedDocumentState( + pageName: 'Page 1', + locks: const PersistentLockState(lockZoom: true), + ); + await fileSystem.initialize(); + await fileSystem.createFile('path/a', existing); + + await DocumentStateRepository( + fileSystem, + settingsProvider: () => + const DocumentStatePersistenceSettings(locks: false), + ).save( + const PersistedDocumentState( + pageName: 'Page 2', + locks: PersistentLockState(lockLayer: true), + ), + pathKey: 'path/a', + ); + + final saved = await fileSystem.getFile('path/a'); + expect(saved?.pageName, 'Page 2'); + expect(saved?.locks, existing.locks); + }); + + test('cleanup removes old and overflowing records', () async { + final repository = DocumentStateRepository( + fileSystem, + settingsProvider: () => const DocumentStatePersistenceSettings( + maxEntries: 2, + maxAgeDays: 30, + ), + ); + await fileSystem.initialize(); + await fileSystem.createFile( + 'path/old', + PersistedDocumentState(updatedAt: DateTime.utc(2026, 1)), + ); + await fileSystem.createFile( + 'path/a', + PersistedDocumentState(updatedAt: DateTime.utc(2026, 5, 1)), + ); + await fileSystem.createFile( + 'path/b', + PersistedDocumentState(updatedAt: DateTime.utc(2026, 5, 2)), + ); + await fileSystem.createFile( + 'path/c', + PersistedDocumentState(updatedAt: DateTime.utc(2026, 5, 3)), + ); + + await repository.cleanup(now: DateTime.utc(2026, 6, 1)); + + expect(await fileSystem.getFile('path/old'), isNull); + expect(await fileSystem.getFile('path/a'), isNull); + expect(await fileSystem.getFile('path/b'), isNotNull); + expect(await fileSystem.getFile('path/c'), isNotNull); + }); + test('writes session state to content and path keys', () async { final transformCubit = TransformCubit(1); final cubit = EditorSessionCubit( From 718e25d4e6eb8895a01d5e98b87bfc7d09d7e3da Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Thu, 2 Jul 2026 18:02:24 +0200 Subject: [PATCH 038/117] Fix performance regressions with big refactor --- app/lib/bloc/document_bloc.dart | 1 + app/lib/cubits/document_save.dart | 266 ++++++++++----------- app/lib/cubits/editor_controller.dart | 7 +- app/lib/cubits/editor_renderer.dart | 17 +- app/lib/cubits/editor_session.dart | 35 ++- app/lib/cubits/editor_tool.dart | 60 ++++- app/lib/handlers/pen.dart | 8 + app/lib/repositories/document_state.dart | 54 ++++- app/lib/views/app_bar.dart | 1 + app/test/bloc/document_bloc_test.dart | 281 ++++++++++++++++++++++- app/test/cubits/editor_session_test.dart | 25 ++ 11 files changed, 592 insertions(+), 163 deletions(-) diff --git a/app/lib/bloc/document_bloc.dart b/app/lib/bloc/document_bloc.dart index 773ee9a14954..2cf5e48fd69d 100644 --- a/app/lib/bloc/document_bloc.dart +++ b/app/lib/bloc/document_bloc.dart @@ -1910,6 +1910,7 @@ class DocumentBloc extends ReplayBloc { location: location, force: force, isAutosave: isAutosave, + editorSessionCubit: _editorController!.editorSessionCubit, ); bool isInBounds(Offset globalPosition) { diff --git a/app/lib/cubits/document_save.dart b/app/lib/cubits/document_save.dart index cbd921133ac6..9eb339d39d9d 100644 --- a/app/lib/cubits/document_save.dart +++ b/app/lib/cubits/document_save.dart @@ -1,149 +1,153 @@ -part of 'editor_runtime.dart'; - -@freezed -sealed class DocumentSaveState with _$DocumentSaveState { - const DocumentSaveState._(); - - const factory DocumentSaveState({ - @Default(false) bool isSaveDelayed, - @Default(AssetLocation(path: '')) AssetLocation location, - Embedding? embedding, - @Default(SaveState.saved) SaveState saved, - @Default(false) bool isCreating, - }) = _DocumentSaveState; - - bool get absolute => saved == SaveState.absoluteRead; -} - -class DocumentSaveCubit extends Cubit { - DocumentSaveCubit( - this.settingsCubit, [ - super.initial = const DocumentSaveState(), - ]); - - final SettingsCubit settingsCubit; - - final savingLock = Lock(); - - void replace(DocumentSaveState state) => emit(state); - - void setSaveState({ - AssetLocation? location, - SaveState? saved, - bool absolute = false, - bool? isCreating, - bool keepRead = false, - }) => emit( - state.copyWith( - location: location ?? state.location, - isCreating: isCreating ?? state.isCreating, - saved: (absolute || (keepRead && state.absolute)) - ? SaveState.absoluteRead - : saved ?? state.saved, - ), - ); - - void setDelayed(bool delayed) => emit(state.copyWith(isSaveDelayed: delayed)); - - ExternalStorage? getRemoteStorage() => - settingsCubit.getRemote(state.location.remote); - - bool hasAutosave(NetworkingService networkingService) => - settingsCubit.state.autosave && - (networkingService.isActive || - !(state.embedding?.save ?? true) || - (!kIsWeb && - !state.absolute && - (state.location.isEmpty || - (state.location.fileType?.isNote() ?? false)) && - (state.location.remote.isEmpty || - (settingsCubit - .getRemote(state.location.remote) - ?.hasDocumentCached(state.location.path) ?? - false)))); - - Future save( - DocumentBloc bloc, - NetworkingService networkingService, { - AssetLocation? location, - bool force = false, - bool isAutosave = false, +part of 'editor_runtime.dart'; + +@freezed +sealed class DocumentSaveState with _$DocumentSaveState { + const DocumentSaveState._(); + + const factory DocumentSaveState({ + @Default(false) bool isSaveDelayed, + @Default(AssetLocation(path: '')) AssetLocation location, + Embedding? embedding, + @Default(SaveState.saved) SaveState saved, + @Default(false) bool isCreating, + }) = _DocumentSaveState; + + bool get absolute => saved == SaveState.absoluteRead; +} + +class DocumentSaveCubit extends Cubit { + DocumentSaveCubit( + this.settingsCubit, [ + super.initial = const DocumentSaveState(), + ]); + + final SettingsCubit settingsCubit; + + final savingLock = Lock(); + + void replace(DocumentSaveState state) => emit(state); + + void setSaveState({ + AssetLocation? location, + SaveState? saved, + bool absolute = false, + bool? isCreating, + bool keepRead = false, + }) => emit( + state.copyWith( + location: location ?? state.location, + isCreating: isCreating ?? state.isCreating, + saved: (absolute || (keepRead && state.absolute)) + ? SaveState.absoluteRead + : saved ?? state.saved, + ), + ); + + void setDelayed(bool delayed) => emit(state.copyWith(isSaveDelayed: delayed)); + + ExternalStorage? getRemoteStorage() => + settingsCubit.getRemote(state.location.remote); + + bool hasAutosave(NetworkingService networkingService) => + settingsCubit.state.autosave && + (networkingService.isActive || + !(state.embedding?.save ?? true) || + (!kIsWeb && + !state.absolute && + (state.location.isEmpty || + (state.location.fileType?.isNote() ?? false)) && + (state.location.remote.isEmpty || + (settingsCubit + .getRemote(state.location.remote) + ?.hasDocumentCached(state.location.path) ?? + false)))); + + Future save( + DocumentBloc bloc, + NetworkingService networkingService, { + AssetLocation? location, + bool force = false, + bool isAutosave = false, + EditorSessionCubit? editorSessionCubit, }) async { final absolute = state.absolute; if (location == null && !force && (state.saved == SaveState.saved || state.saved == SaveState.absoluteRead)) { + await editorSessionCubit?.saveNow(); return state.location; - } - if (networkingService.isClient) { - return AssetLocation.empty; - } - if (state.isSaveDelayed && isAutosave) { - return state.location; - } - final storage = getRemoteStorage(); - final fileSystem = bloc.state.fileSystem.buildDocumentSystem(storage); - final isDelayed = settingsCubit.state.delayedAutosave; - if (isDelayed && isAutosave) { - final seconds = max(0, settingsCubit.state.autosaveDelaySeconds); - setDelayed(true); - await Future.delayed(Duration(seconds: seconds)); - if (!state.isSaveDelayed) { - return state.location; - } + } + if (networkingService.isClient) { + return AssetLocation.empty; + } + if (state.isSaveDelayed && isAutosave) { + return state.location; + } + final storage = getRemoteStorage(); + final fileSystem = bloc.state.fileSystem.buildDocumentSystem(storage); + final isDelayed = settingsCubit.state.delayedAutosave; + if (isDelayed && isAutosave) { + final seconds = max(0, settingsCubit.state.autosaveDelaySeconds); + setDelayed(true); + await Future.delayed(Duration(seconds: seconds)); + if (!state.isSaveDelayed) { + return state.location; + } } return savingLock.synchronized(() async { if (location == null && !force && (state.saved == SaveState.saved || state.saved == SaveState.absoluteRead)) { + await editorSessionCubit?.saveNow(); return state.location; } var current = location ?? state.location; if (isClosed) { return current; - } - setSaveState(saved: SaveState.saving, location: current); - setDelayed(false); - final blocState = bloc.state; - final currentData = await blocState.saveData(); - if (isClosed) { - return current; - } - if (currentData == null || state.embedding != null) { - setSaveState(saved: SaveState.saved); - return AssetLocation.empty; - } - if (absolute || !(current.fileType?.isNote() ?? false)) { - final file = await compute(_toFile, (currentData, false)); - final document = await fileSystem.createFileWithName( - name: currentData.name, - suffix: '.bfly', - directory: absolute - ? null - : current.fileExtension.isEmpty - ? state.location.path - : state.location.parent, - file, - ); - current = document.location; - } else { - final file = await compute(_toFile, ( - currentData, - current.fileType == AssetFileType.textNote, - )); - await fileSystem.updateFile(current.path, file); - } - settingsCubit.addRecentHistory(current); - if (isClosed) { - return current; - } - setSaveState( - saved: state.saved == SaveState.saving ? SaveState.saved : state.saved, - location: current, - ); - return current; - }); - } -} + } + setSaveState(saved: SaveState.saving, location: current); + setDelayed(false); + final blocState = bloc.state; + final currentData = await blocState.saveData(); + if (isClosed) { + return current; + } + if (currentData == null || state.embedding != null) { + setSaveState(saved: SaveState.saved); + return AssetLocation.empty; + } + if (absolute || !(current.fileType?.isNote() ?? false)) { + final file = await compute(_toFile, (currentData, false)); + final document = await fileSystem.createFileWithName( + name: currentData.name, + suffix: '.bfly', + directory: absolute + ? null + : current.fileExtension.isEmpty + ? state.location.path + : state.location.parent, + file, + ); + current = document.location; + } else { + final file = await compute(_toFile, ( + currentData, + current.fileType == AssetFileType.textNote, + )); + await fileSystem.updateFile(current.path, file); + } + settingsCubit.addRecentHistory(current); + await editorSessionCubit?.saveNow(); + if (isClosed) { + return current; + } + setSaveState( + saved: state.saved == SaveState.saving ? SaveState.saved : state.saved, + location: current, + ); + return current; + }); + } +} diff --git a/app/lib/cubits/editor_controller.dart b/app/lib/cubits/editor_controller.dart index a132171d7c6e..528ed1ab0137 100644 --- a/app/lib/cubits/editor_controller.dart +++ b/app/lib/cubits/editor_controller.dart @@ -290,7 +290,12 @@ class EditorController implements EditorRuntimeContext { toolCubit.updateIndex(this, bloc); } if (saveCubit.hasAutosave(networkingService)) { - saveCubit.save(bloc, networkingService, isAutosave: true); + saveCubit.save( + bloc, + networkingService, + isAutosave: true, + editorSessionCubit: editorSessionCubit, + ); } } diff --git a/app/lib/cubits/editor_renderer.dart b/app/lib/cubits/editor_renderer.dart index 40fa97b533a5..b9754910f190 100644 --- a/app/lib/cubits/editor_renderer.dart +++ b/app/lib/cubits/editor_renderer.dart @@ -12,7 +12,7 @@ sealed class RendererRuntimeState with _$RendererRuntimeState { Map get allRendererStates => { ...rendererStates, - ...?temporaryRendererStates, + ...(temporaryRendererStates ?? const {}), }; } @@ -112,6 +112,21 @@ class RendererCubit extends Cubit { initializedElements.removeAll(renderers); } + bool rendererStateChangesAffectBaked( + Map current, + Map next, + ) { + final changedIds = { + ...current.keys, + ...next.keys, + }.where((id) => current[id] != next[id]); + if (changedIds.isEmpty) return false; + final bakedIds = state.cameraViewport.bakedElements + .map((renderer) => renderer.id) + .toSet(); + return changedIds.any(bakedIds.contains); + } + Rect getViewportRect(TransformCubit transformCubit, {Size? viewportSize}) { var size = viewportSize ?? state.cameraViewport.toSize(); final transform = transformCubit.state; diff --git a/app/lib/cubits/editor_session.dart b/app/lib/cubits/editor_session.dart index c4e465b4267c..5403a26ea15c 100644 --- a/app/lib/cubits/editor_session.dart +++ b/app/lib/cubits/editor_session.dart @@ -29,6 +29,9 @@ class EditorSessionCubit extends Cubit { final bool persist; StreamSubscription? _transformSubscription; Timer? _saveDebounce; + Future? _saveFuture; + PersistedDocumentState? _pendingSave; + var _dirty = false; static PersistedDocumentState buildInitial({ PersistedDocumentState? restored, @@ -99,19 +102,19 @@ class EditorSessionCubit extends Cubit { ); if (state.camera == camera) return; emit(state.copyWith(camera: camera)); - scheduleSave(); + _dirty = true; } void updatePage(String pageName) { if (state.pageName == pageName) return; emit(state.copyWith(pageName: pageName)); - unawaited(saveNow()); + _dirty = true; } void updateLocks(PersistentLockState locks) { if (state.locks == locks) return; emit(state.copyWith(locks: locks)); - unawaited(saveNow()); + _dirty = true; } void updateSelectedTool(Tool? tool, int? index) { @@ -121,7 +124,7 @@ class EditorSessionCubit extends Cubit { ); if (state.selectedTool == selection) return; emit(state.copyWith(selectedTool: selection)); - unawaited(saveNow()); + _dirty = true; } void updateNavigator({bool? enabled, NavigatorPage? page}) { @@ -133,7 +136,7 @@ class EditorSessionCubit extends Cubit { ); if (next == state) return; emit(next); - unawaited(saveNow()); + _dirty = true; } void updateLayer({ @@ -150,7 +153,7 @@ class EditorSessionCubit extends Cubit { ); if (next == state) return; emit(next); - unawaited(saveNow()); + _dirty = true; } void updateAreaNavigator({bool? create, bool? exact, bool? ask}) { @@ -163,7 +166,7 @@ class EditorSessionCubit extends Cubit { ); if (next == state) return; emit(next); - unawaited(saveNow()); + _dirty = true; } void scheduleSave() { @@ -177,9 +180,25 @@ class EditorSessionCubit extends Cubit { if (!persist) return; _saveDebounce?.cancel(); _saveDebounce = null; + if (!_dirty && _pendingSave == null && _saveFuture == null) return; final next = state.touch(pathKey: pathKey, contentHash: contentHash); + _dirty = false; emit(next); - await repository.save(next, contentHash: contentHash, pathKey: pathKey); + _pendingSave = next; + _saveFuture ??= _drainSaves(); + await _saveFuture; + } + + Future _drainSaves() async { + try { + while (_pendingSave != null) { + final next = _pendingSave!; + _pendingSave = null; + await repository.save(next, contentHash: contentHash, pathKey: pathKey); + } + } finally { + _saveFuture = null; + } } @override diff --git a/app/lib/cubits/editor_tool.dart b/app/lib/cubits/editor_tool.dart index f3a8dd10c7cf..9e644ef9a580 100644 --- a/app/lib/cubits/editor_tool.dart +++ b/app/lib/cubits/editor_tool.dart @@ -861,16 +861,30 @@ class ToolCubit extends Cubit { toggleableForegrounds[index] = foregrounds; } final rendererStates = state.handler.rendererStates; - final temporaryRendererStates = state.temporaryHandler?.rendererStates; + final currentTemporaryRendererStates = + controller.rendererCubit.state.temporaryRendererStates ?? + const {}; + final temporaryRendererStates = + state.temporaryHandler?.rendererStates ?? + const {}; final statesChanged = !mapEq.equals( controller.rendererCubit.state.rendererStates, rendererStates, ); final temporaryStatesChanged = !mapEq.equals( - controller.rendererCubit.state.temporaryRendererStates, + currentTemporaryRendererStates, temporaryRendererStates, ); final shouldBake = statesChanged || temporaryStatesChanged; + final resetBake = + controller.rendererCubit.rendererStateChangesAffectBaked( + controller.rendererCubit.state.rendererStates, + rendererStates, + ) || + controller.rendererCubit.rendererStateChangesAffectBaked( + currentTemporaryRendererStates, + temporaryRendererStates, + ); setForegrounds( temporaryForegrounds: temporaryForegrounds, toggleableForegrounds: toggleableForegrounds, @@ -884,16 +898,22 @@ class ToolCubit extends Cubit { : controller.rendererCubit.state.rendererStates, temporaryRendererStates: temporaryStatesChanged ? temporaryRendererStates - : controller.rendererCubit.state.temporaryRendererStates, + : currentTemporaryRendererStates, ); if (allowBake) { - if (shouldBake) { + if (shouldBake && resetBake) { return controller.rendererCubit.bake( controller, blocState, reset: true, ); - } else if (!controller.rendererCubit.state.cameraViewport.baked) { + } else if (!controller.rendererCubit.state.cameraViewport.baked || + controller + .rendererCubit + .state + .cameraViewport + .unbakedElements + .isNotEmpty) { return controller.rendererCubit.delayedBake(controller, blocState); } } @@ -971,15 +991,29 @@ class ToolCubit extends Cubit { const mapEq = MapEquality(); final rendererStates = state.handler.rendererStates; - final temporaryRendererStates = state.temporaryHandler?.rendererStates; + final currentTemporaryRendererStates = + controller.rendererCubit.state.temporaryRendererStates ?? + const {}; + final temporaryRendererStates = + state.temporaryHandler?.rendererStates ?? + const {}; final statesChanged = !mapEq.equals( controller.rendererCubit.state.rendererStates, rendererStates, ); final temporaryStatesChanged = !mapEq.equals( - controller.rendererCubit.state.temporaryRendererStates, + currentTemporaryRendererStates, temporaryRendererStates, ); + final resetBake = + controller.rendererCubit.rendererStateChangesAffectBaked( + controller.rendererCubit.state.rendererStates, + rendererStates, + ) || + controller.rendererCubit.rendererStateChangesAffectBaked( + currentTemporaryRendererStates, + temporaryRendererStates, + ); setForegrounds( foregrounds: foregrounds, @@ -993,11 +1027,19 @@ class ToolCubit extends Cubit { : controller.rendererCubit.state.rendererStates, temporaryRendererStates: temporaryStatesChanged ? temporaryRendererStates - : controller.rendererCubit.state.temporaryRendererStates, + : currentTemporaryRendererStates, ); - if (statesChanged || temporaryStatesChanged) { + if ((statesChanged || temporaryStatesChanged) && resetBake) { await controller.rendererCubit.bake(controller, blocState, reset: true); + } else if (!controller.rendererCubit.state.cameraViewport.baked || + controller + .rendererCubit + .state + .cameraViewport + .unbakedElements + .isNotEmpty) { + await controller.rendererCubit.delayedBake(controller, blocState); } } diff --git a/app/lib/handlers/pen.dart b/app/lib/handlers/pen.dart index cbfeed28107e..fcf18db2e3bc 100644 --- a/app/lib/handlers/pen.dart +++ b/app/lib/handlers/pen.dart @@ -123,6 +123,14 @@ class PenHandler extends Handler with ColoredHandler { CameraViewport newViewport, ) async { if (_submittedElements.isEmpty) return; + final submittedIds = _submittedElements.map((e) => e.id).nonNulls.toSet(); + final viewportIds = [ + ...newViewport.bakedElements, + ...newViewport.unbakedElements, + ].map((renderer) => renderer.element.id).nonNulls.toSet(); + if (submittedIds.isNotEmpty && viewportIds.containsAll(submittedIds)) { + return; + } if (_currentlyBaking) return; _currentlyBaking = true; _submittedElements.clear(); diff --git a/app/lib/repositories/document_state.dart b/app/lib/repositories/document_state.dart index b0f3d5ec5cbb..76a1f7354b75 100644 --- a/app/lib/repositories/document_state.dart +++ b/app/lib/repositories/document_state.dart @@ -1,13 +1,19 @@ +import 'dart:async'; + import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/models/persisted_document_state.dart'; +import 'package:synchronized/synchronized.dart'; class DocumentStateRepository { DocumentStateRepository(this.fileSystem, {this.settingsProvider}); final DocumentStateFileSystem fileSystem; final DocumentStatePersistenceSettings Function()? settingsProvider; + final _lock = Lock(); var _cleanupStarted = false; + DateTime? _lastCleanupAt; + Future? _scheduledCleanup; DocumentStatePersistenceSettings get _settings => settingsProvider?.call() ?? const DocumentStatePersistenceSettings(); @@ -16,28 +22,28 @@ class DocumentStateRepository { String? contentHash, String? pathKey, bool allowContentHash = true, - }) async { + }) => _lock.synchronized(() async { final settings = _settings; if (!settings.enabled) return null; await fileSystem.initialize(); if (allowContentHash && contentHash != null) { - final byContent = await fileSystem.getFile( + final byContent = await _getFileOrNull( documentStateContentKey(contentHash), ); if (byContent != null) return _applySettings(byContent, settings); } if (pathKey != null) { - final byPath = await fileSystem.getFile(pathKey); + final byPath = await _getFileOrNull(pathKey); return byPath == null ? null : _applySettings(byPath, settings); } return null; - } + }); Future save( PersistedDocumentState state, { String? contentHash, String? pathKey, - }) async { + }) => _lock.synchronized(() async { final settings = _settings; if (!settings.enabled) return; await fileSystem.initialize(); @@ -47,14 +53,14 @@ class DocumentStateRepository { if (pathKey != null) { await _put(pathKey, state, settings); } - await _cleanupAfterSave(contentHash: contentHash, pathKey: pathKey); - } + _scheduleCleanupAfterSave(contentHash: contentHash, pathKey: pathKey); + }); Future cleanup({ String? contentHash, String? pathKey, DateTime? now, - }) async { + }) => _lock.synchronized(() async { final settings = _settings; await fileSystem.initialize(); final keys = await fileSystem.getKeys(); @@ -67,7 +73,7 @@ class DocumentStateRepository { } final records = <({String key, PersistedDocumentState state})>[]; for (final key in keys) { - final state = await fileSystem.getFile(key); + final state = await _getFileOrNull(key); if (state == null) continue; records.add((key: key, state: state)); } @@ -83,7 +89,7 @@ class DocumentStateRepository { } final remaining = <({String key, PersistedDocumentState state})>[]; for (final key in await fileSystem.getKeys()) { - final state = await fileSystem.getFile(key); + final state = await _getFileOrNull(key); if (state != null) remaining.add((key: key, state: state)); } remaining.sort((a, b) { @@ -101,14 +107,14 @@ class DocumentStateRepository { for (final record in removable.take(overflow)) { await fileSystem.deleteFile(record.key); } - } + }); Future _put( String key, PersistedDocumentState state, DocumentStatePersistenceSettings settings, ) async { - final existing = await fileSystem.getFile(key); + final existing = await _getFileOrNull(key); final next = _mergeEnabled(existing, state, settings); if (existing != null || await fileSystem.hasKey(key)) { await fileSystem.updateFile(key, next); @@ -155,11 +161,35 @@ class DocumentStateRepository { ); } + Future _getFileOrNull(String key) async { + try { + return await fileSystem.getFile(key); + } on FormatException { + return null; + } + } + + void _scheduleCleanupAfterSave({String? contentHash, String? pathKey}) { + final now = DateTime.now().toUtc(); + final last = _lastCleanupAt; + if (last != null && now.difference(last) < const Duration(minutes: 10)) { + return; + } + if (_scheduledCleanup != null) return; + _scheduledCleanup = + Future(() { + return _cleanupAfterSave(contentHash: contentHash, pathKey: pathKey); + }).whenComplete(() { + _scheduledCleanup = null; + }); + } + Future _cleanupAfterSave({String? contentHash, String? pathKey}) async { if (_cleanupStarted) return; _cleanupStarted = true; try { await cleanup(contentHash: contentHash, pathKey: pathKey); + _lastCleanupAt = DateTime.now().toUtc(); } finally { _cleanupStarted = false; } diff --git a/app/lib/views/app_bar.dart b/app/lib/views/app_bar.dart index 605286859ac8..854284ba70ef 100644 --- a/app/lib/views/app_bar.dart +++ b/app/lib/views/app_bar.dart @@ -288,6 +288,7 @@ class _AppBarTitleState extends State<_AppBarTitle> { cubit.networkingService, location: newLocation, force: true, + editorSessionCubit: cubit.editorSessionCubit, ); if (!location.isEmpty && !savedLocation.isEmpty && diff --git a/app/test/bloc/document_bloc_test.dart b/app/test/bloc/document_bloc_test.dart index 7385ba6da2e7..e7b8789aaf52 100644 --- a/app/test/bloc/document_bloc_test.dart +++ b/app/test/bloc/document_bloc_test.dart @@ -7,6 +7,7 @@ import 'package:butterfly/bloc/document_bloc.dart'; import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/transform.dart'; +import 'package:butterfly/handlers/handler.dart'; import 'package:butterfly/models/viewport.dart'; import 'package:butterfly/renderers/renderer.dart'; import 'package:butterfly/services/asset.dart'; @@ -30,6 +31,7 @@ class _VisibleTrackingRenderer extends Renderer { int onVisibleCalls = 0; int onHiddenCalls = 0; int disposeCalls = 0; + int buildCalls = 0; CameraTransform? lastVisibleTransform; Size? lastVisibleSize; @@ -79,7 +81,9 @@ class _VisibleTrackingRenderer extends Renderer { CameraTransform transform, [ ColorScheme? colorScheme, bool foreground = false, - ]) {} + ]) { + buildCalls++; + } @override void dispose() { @@ -720,6 +724,72 @@ void main() { expect(viewport.unbakedElements, hasLength(2)); }); + test( + 'pen viewport update keeps submitted stroke until renderer is reported', + () async { + final state = bloc.state as DocumentLoadSuccess; + final handler = PenHandler(PenTool(id: 'pen')); + final element = PenElement( + id: 'stroke', + points: const [PathPoint(10, 20), PathPoint(40, 20)], + ); + handler.elements[1] = element; + + await handler.submitElements(bloc, [1]); + + expect( + handler.createForegrounds( + editorController, + state.data, + state.page, + state.info, + ), + hasLength(1), + ); + + final renderer = Renderer.fromInstance( + element, + state.currentLayer, + ); + await renderer.setup( + editorController.transformCubit, + state.data, + state.assetService, + state.page, + ); + await handler.onViewportUpdated( + const CameraViewport.unbaked(), + CameraViewport.unbaked( + unbakedElements: [renderer], + visibleElements: [renderer], + visibleUnbakedElements: [renderer], + ), + ); + + expect( + handler.createForegrounds( + editorController, + state.data, + state.page, + state.info, + ), + hasLength(1), + ); + expect(handler.onRenderersCreated(state.page, [renderer]), isTrue); + expect( + handler.createForegrounds( + editorController, + state.data, + state.page, + state.info, + ), + isEmpty, + ); + + renderer.dispose(); + }, + ); + test('bake records only elements visible in the current viewport', () async { await bloc.close(); await editorController.close(); @@ -789,6 +859,215 @@ void main() { expect(viewport.visibleUnbakedElements, isEmpty); }); + test('incremental bake does not rebuild already baked renderers', () async { + await bloc.close(); + await editorController.close(); + + final existingElement = ShapeElement( + id: 'existing', + firstPosition: const Point(10, 10), + secondPosition: const Point(20, 20), + ); + final addedElement = ShapeElement( + id: 'added', + firstPosition: const Point(30, 30), + secondPosition: const Point(40, 40), + ); + final existingRenderer = _VisibleTrackingRenderer(existingElement, 'layer'); + final addedRenderer = _VisibleTrackingRenderer(addedElement, 'layer'); + final page = DocumentPage( + layers: [ + DocumentLayer(id: 'layer', content: [existingElement, addedElement]), + ], + ); + var data = NoteData(Archive()); + final (nextData, pageName) = data.setPage(page, 'Page 1'); + data = nextData; + editorController = EditorController( + settingsCubit, + TransformCubit(1), + CameraViewport.unbaked( + unbakedElements: [existingRenderer], + visibleElements: [existingRenderer], + visibleUnbakedElements: [existingRenderer], + width: 100, + height: 100, + ), + ); + bloc = DocumentBloc( + fileSystem, + editorController, + windowCubit, + data, + const AssetLocation(path: 'test-note.bfly'), + null, + page, + pageName, + ); + + final state = bloc.state as DocumentLoadSuccess; + await editorController.rendererCubit.bake( + editorController, + state, + viewportSize: const Size(100, 100), + pixelRatio: 1, + reset: true, + ); + final existingBuildsAfterReset = existingRenderer.buildCalls; + + await addedRenderer.setup( + editorController.transformCubit, + state.data, + state.assetService, + state.page, + ); + await editorController.rendererCubit.addUnbaked(editorController, state, [ + addedRenderer, + ]); + await editorController.rendererCubit.bake( + editorController, + state, + viewportSize: const Size(100, 100), + pixelRatio: 1, + ); + + expect(existingRenderer.buildCalls, existingBuildsAfterReset); + expect(addedRenderer.buildCalls, greaterThan(0)); + }); + + test('tool refresh without temporary handler does not reset bake', () async { + await bloc.close(); + await editorController.close(); + + final element = ShapeElement( + id: 'existing', + firstPosition: const Point(10, 10), + secondPosition: const Point(20, 20), + ); + final renderer = _VisibleTrackingRenderer(element, 'layer'); + final page = DocumentPage( + layers: [ + DocumentLayer(id: 'layer', content: [element]), + ], + ); + var data = NoteData(Archive()); + final (nextData, pageName) = data.setPage(page, 'Page 1'); + data = nextData; + editorController = EditorController( + settingsCubit, + TransformCubit(1), + CameraViewport.unbaked( + unbakedElements: [renderer], + visibleElements: [renderer], + visibleUnbakedElements: [renderer], + width: 100, + height: 100, + ), + ); + bloc = DocumentBloc( + fileSystem, + editorController, + windowCubit, + data, + const AssetLocation(path: 'test-note.bfly'), + null, + page, + pageName, + ); + + final state = bloc.state as DocumentLoadSuccess; + await editorController.rendererCubit.bake( + editorController, + state, + viewportSize: const Size(100, 100), + pixelRatio: 1, + reset: true, + ); + final buildsAfterBake = renderer.buildCalls; + + await editorController.toolCubit.refresh(editorController, state); + + expect(renderer.buildCalls, buildsAfterBake); + }); + + test( + 'creating pen stroke through bloc keeps the next bake incremental', + () async { + await bloc.close(); + await editorController.close(); + + final existingElement = ShapeElement( + id: 'existing', + firstPosition: const Point(10, 10), + secondPosition: const Point(20, 20), + ); + final existingRenderer = _VisibleTrackingRenderer( + existingElement, + 'layer', + ); + final page = DocumentPage( + layers: [ + DocumentLayer(id: 'layer', content: [existingElement]), + ], + ); + var data = NoteData(Archive()); + final (nextData, pageName) = data.setPage(page, 'Page 1'); + data = nextData; + editorController = EditorController( + settingsCubit, + TransformCubit(1), + CameraViewport.unbaked( + unbakedElements: [existingRenderer], + visibleElements: [existingRenderer], + visibleUnbakedElements: [existingRenderer], + width: 100, + height: 100, + ), + ); + bloc = DocumentBloc( + fileSystem, + editorController, + windowCubit, + data, + const AssetLocation(path: 'test-note.bfly'), + null, + page, + pageName, + ); + final state = bloc.state as DocumentLoadSuccess; + final penHandler = PenHandler(PenTool(id: 'pen')); + await editorController.toolCubit.changeTool( + editorController, + bloc, + handler: penHandler, + allowBake: false, + ); + await editorController.rendererCubit.bake( + editorController, + state, + viewportSize: const Size(100, 100), + pixelRatio: 1, + reset: true, + ); + final buildsAfterInitialBake = existingRenderer.buildCalls; + + penHandler.elements[1] = PenElement( + id: 'stroke', + points: const [PathPoint(30, 30), PathPoint(40, 40)], + ); + await penHandler.submitElements(bloc, [1]); + await _settleBlocEvents(); + await Future.delayed(const Duration(milliseconds: 150)); + await _settleBlocEvents(); + + expect(existingRenderer.buildCalls, buildsAfterInitialBake); + expect( + editorController.rendererCubit.renderers.map((e) => e.element.id), + contains('stroke'), + ); + }, + ); + test('bake refreshes cached viewport when pixel ratio changes', () async { await bloc.close(); await editorController.close(); diff --git a/app/test/cubits/editor_session_test.dart b/app/test/cubits/editor_session_test.dart index 10509492ee9f..a3c5452c5dc8 100644 --- a/app/test/cubits/editor_session_test.dart +++ b/app/test/cubits/editor_session_test.dart @@ -231,6 +231,31 @@ void main() { await transformCubit.close(); }); + test('camera changes stay in memory until session is flushed', () async { + final transformCubit = TransformCubit(1); + final cubit = EditorSessionCubit( + repository: DocumentStateRepository(fileSystem), + transformCubit: transformCubit, + initialState: const PersistedDocumentState(pageName: 'Page 1'), + pathKey: 'path/a', + contentHash: 'hash-a', + ); + + transformCubit.teleport(const Offset(10, 20), 2); + await Future.delayed(const Duration(milliseconds: 300)); + + expect(await fileSystem.getFile('path/a'), isNull); + await cubit.saveNow(); + + final saved = await fileSystem.getFile('path/a'); + expect(saved?.camera.positionX, 10); + expect(saved?.camera.positionY, 20); + expect(saved?.camera.zoom, 2); + + await cubit.close(); + await transformCubit.close(); + }); + test('does not modify document files when session state changes', () async { final documentSystem = buildMockDocumentFileSystem(); final original = NoteFile(Uint8List.fromList([1, 2, 3])); From b8d4a10241f93cf0750f5f96636fe17dfeb6404e Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Thu, 2 Jul 2026 18:19:33 +0200 Subject: [PATCH 039/117] Fix polygon disappears --- app/lib/handlers/polygon.dart | 8 ++++++- app/test/handlers/polygon_handler_test.dart | 24 +++++++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/app/lib/handlers/polygon.dart b/app/lib/handlers/polygon.dart index 81b35c8b3560..c2e329b4c638 100644 --- a/app/lib/handlers/polygon.dart +++ b/app/lib/handlers/polygon.dart @@ -119,7 +119,13 @@ class PolygonHandler extends Handler with ColoredHandler { final element = _element; if (element != null) { _element = element.copyWith(property: tool.property); - unawaited(bloc.currentIndexCubit.refreshToolbar(bloc)); + unawaited( + bloc.editorController.toolCubit.updateHandler( + bloc, + bloc.editorController.rendererCubit, + this, + ), + ); unawaited(bloc.refreshForegrounds()); return; } diff --git a/app/test/handlers/polygon_handler_test.dart b/app/test/handlers/polygon_handler_test.dart index c6f11f681644..d82cffdab437 100644 --- a/app/test/handlers/polygon_handler_test.dart +++ b/app/test/handlers/polygon_handler_test.dart @@ -132,7 +132,7 @@ void main() { final (data, pageName) = NoteData(Archive()).setPage(page, 'Page 1'); bloc = DocumentBloc( fileSystem, - currentIndexCubit, + editorController, windowCubit, data, const AssetLocation(path: 'test-note.bfly'), @@ -143,20 +143,26 @@ void main() { final handler = PolygonHandler( PolygonTool(id: 'polygon-tool', property: originalProperty), )..editElement(element); - final toolbar = handler.getToolbar(bloc!) as PolygonToolbarView; + await editorController.toolCubit.updateHandler( + bloc!, + editorController.rendererCubit, + handler, + ); + await bloc!.refreshForegrounds(); + + final toolbar = + editorController.toolCubit.state.toolbar as PolygonToolbarView; toolbar.onToolChanged(toolbar.tool.copyWith(property: updatedProperty)); await _settleBlocEvents(); + await bloc!.refreshForegrounds(); - final polygon = handler - .createForegrounds( - currentIndexCubit, - (bloc!.state as DocumentLoadSuccess).data, - (bloc!.state as DocumentLoadSuccess).page, - (bloc!.state as DocumentLoadSuccess).info, - ) + final polygon = editorController.toolCubit.state.foregrounds .whereType() .single .element; expect(polygon.property, updatedProperty); + expect(editorController.rendererCubit.state.rendererStates, { + 'polygon': RendererState.hidden, + }); }); } From 3210be5c3e15d858179d9369919943f82b2699f7 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Thu, 2 Jul 2026 21:53:11 +0200 Subject: [PATCH 040/117] Fix persistent state content hash --- app/lib/cubits/document_save.dart | 10 +- app/lib/cubits/editor_session.dart | 66 ++++++-- app/lib/repositories/document_state.dart | 198 ++++++++++++----------- app/lib/views/main.dart | 2 +- app/test/bloc/document_bloc_test.dart | 10 +- app/test/cubits/editor_session_test.dart | 89 +++++++++- 6 files changed, 251 insertions(+), 124 deletions(-) diff --git a/app/lib/cubits/document_save.dart b/app/lib/cubits/document_save.dart index 9eb339d39d9d..b02dab35901c 100644 --- a/app/lib/cubits/document_save.dart +++ b/app/lib/cubits/document_save.dart @@ -118,6 +118,7 @@ class DocumentSaveCubit extends Cubit { setSaveState(saved: SaveState.saved); return AssetLocation.empty; } + Uint8List currentDataBytes; if (absolute || !(current.fileType?.isNote() ?? false)) { final file = await compute(_toFile, (currentData, false)); final document = await fileSystem.createFileWithName( @@ -131,15 +132,22 @@ class DocumentSaveCubit extends Cubit { file, ); current = document.location; + currentDataBytes = file.data; } else { final file = await compute(_toFile, ( currentData, current.fileType == AssetFileType.textNote, )); await fileSystem.updateFile(current.path, file); + currentDataBytes = file.data; } settingsCubit.addRecentHistory(current); - await editorSessionCubit?.saveNow(); + await editorSessionCubit?.saveNow( + pathKey: documentStatePathKeyOrNull(current), + contentHash: documentStateContentKey( + documentStateContentHash(currentDataBytes), + ), + ); if (isClosed) { return current; } diff --git a/app/lib/cubits/editor_session.dart b/app/lib/cubits/editor_session.dart index 5403a26ea15c..5f821bcdf64a 100644 --- a/app/lib/cubits/editor_session.dart +++ b/app/lib/cubits/editor_session.dart @@ -14,23 +14,24 @@ class EditorSessionCubit extends Cubit { required this.repository, required TransformCubit transformCubit, required PersistedDocumentState initialState, - required this.pathKey, - required this.contentHash, - this.persist = true, + String? pathKey, + String? contentHash, }) : _transformCubit = transformCubit, - super(initialState.touch(pathKey: pathKey, contentHash: contentHash)) { + super(initialState) { _transformSubscription = transformCubit.stream.listen(_onTransformChanged); } final DocumentStateRepository repository; final TransformCubit _transformCubit; - final String? pathKey; - final String? contentHash; - final bool persist; StreamSubscription? _transformSubscription; Timer? _saveDebounce; Future? _saveFuture; - PersistedDocumentState? _pendingSave; + ({ + PersistedDocumentState state, + bool persistentChanged, + PersistedDocumentState? previousState, + })? + _pendingSave; var _dirty = false; static PersistedDocumentState buildInitial({ @@ -176,15 +177,31 @@ class EditorSessionCubit extends Cubit { }); } - Future saveNow() async { - if (!persist) return; + Future saveNow({String? pathKey, String? contentHash}) async { + final persistentChanged = _dirty; _saveDebounce?.cancel(); _saveDebounce = null; - if (!_dirty && _pendingSave == null && _saveFuture == null) return; - final next = state.touch(pathKey: pathKey, contentHash: contentHash); + final pathChanged = pathKey != null && pathKey != state.pathKey; + final contentChanged = + contentHash != null && contentHash != state.contentHash; + if (!pathChanged && !contentChanged && !persistentChanged) { + return; + } + + final next = state.copyWith( + pathKey: pathKey ?? state.pathKey, + contentHash: contentHash ?? state.contentHash, + updatedAt: DateTime.now().toUtc(), + ); + final previousState = state; _dirty = false; emit(next); - _pendingSave = next; + _pendingSave = ( + state: next, + previousState: previousState, + persistentChanged: + persistentChanged || (_pendingSave?.persistentChanged ?? false), + ); _saveFuture ??= _drainSaves(); await _saveFuture; } @@ -192,9 +209,27 @@ class EditorSessionCubit extends Cubit { Future _drainSaves() async { try { while (_pendingSave != null) { - final next = _pendingSave!; + final pending = _pendingSave!; + final next = pending.state; + final previousState = pending.previousState; _pendingSave = null; - await repository.save(next, contentHash: contentHash, pathKey: pathKey); + + try { + await repository.save( + next, + contentKey: next.contentHash, + pathKey: next.pathKey, + previousContentKey: previousState?.contentHash, + previousPathKey: previousState?.pathKey, + persistentChanged: pending.persistentChanged, + ); + } catch (e, stackTrace) { + debugPrintStack( + label: 'Error saving document state', + stackTrace: stackTrace, + ); + rethrow; + } } } finally { _saveFuture = null; @@ -205,7 +240,6 @@ class EditorSessionCubit extends Cubit { Future close() async { _saveDebounce?.cancel(); _saveDebounce = null; - if (persist) await saveNow(); await _transformSubscription?.cancel(); return super.close(); } diff --git a/app/lib/repositories/document_state.dart b/app/lib/repositories/document_state.dart index 76a1f7354b75..a5a415ff4620 100644 --- a/app/lib/repositories/document_state.dart +++ b/app/lib/repositories/document_state.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/models/persisted_document_state.dart'; +import 'package:flutter/foundation.dart'; import 'package:synchronized/synchronized.dart'; class DocumentStateRepository { @@ -26,100 +27,119 @@ class DocumentStateRepository { final settings = _settings; if (!settings.enabled) return null; await fileSystem.initialize(); + if (pathKey != null) { + final byPath = await _getFileOrNull(pathKey); + if (byPath != null) return _applySettings(byPath, settings); + } if (allowContentHash && contentHash != null) { final byContent = await _getFileOrNull( documentStateContentKey(contentHash), ); if (byContent != null) return _applySettings(byContent, settings); } - if (pathKey != null) { - final byPath = await _getFileOrNull(pathKey); - return byPath == null ? null : _applySettings(byPath, settings); - } return null; }); Future save( PersistedDocumentState state, { - String? contentHash, + String? contentKey, String? pathKey, + String? previousContentKey, + String? previousPathKey, + required bool persistentChanged, }) => _lock.synchronized(() async { final settings = _settings; if (!settings.enabled) return; await fileSystem.initialize(); - if (contentHash != null) { - await _put(documentStateContentKey(contentHash), state, settings); - } - if (pathKey != null) { - await _put(pathKey, state, settings); - } - _scheduleCleanupAfterSave(contentHash: contentHash, pathKey: pathKey); - }); - - Future cleanup({ - String? contentHash, - String? pathKey, - DateTime? now, - }) => _lock.synchronized(() async { - final settings = _settings; - await fileSystem.initialize(); - final keys = await fileSystem.getKeys(); - final protectedKeys = {}; - if (contentHash != null) { - protectedKeys.add(documentStateContentKey(contentHash)); - } - if (pathKey != null) { - protectedKeys.add(pathKey); - } - final records = <({String key, PersistedDocumentState state})>[]; - for (final key in keys) { - final state = await _getFileOrNull(key); - if (state == null) continue; - records.add((key: key, state: state)); - } - final cutoff = (now ?? DateTime.now().toUtc()).subtract( - Duration(days: settings.maxAgeDays), - ); - for (final record in records) { - final updatedAt = record.state.updatedAt; - if (protectedKeys.contains(record.key) || updatedAt == null) continue; - if (updatedAt.isBefore(cutoff)) { - await fileSystem.deleteFile(record.key); - } - } - final remaining = <({String key, PersistedDocumentState state})>[]; - for (final key in await fileSystem.getKeys()) { - final state = await _getFileOrNull(key); - if (state != null) remaining.add((key: key, state: state)); - } - remaining.sort((a, b) { - final aTime = a.state.updatedAt ?? DateTime.fromMillisecondsSinceEpoch(0); - final bTime = b.state.updatedAt ?? DateTime.fromMillisecondsSinceEpoch(0); - return aTime.compareTo(bTime); - }); - final removable = remaining - .where((record) => !protectedKeys.contains(record.key)) - .toList(); - final overflow = (remaining.length - settings.maxEntries).clamp( - 0, - removable.length, + await _updateFile(previousPathKey, pathKey, state, persistentChanged); + await _updateFile(previousContentKey, contentKey, state, persistentChanged); + _scheduleCleanupAfterSave( + contentHash: state.contentHash, + pathKey: state.pathKey, ); - for (final record in removable.take(overflow)) { - await fileSystem.deleteFile(record.key); - } }); - Future _put( - String key, + Future cleanup({String? contentHash, String? pathKey, DateTime? now}) => + _lock.synchronized(() async { + final settings = _settings; + await fileSystem.initialize(); + final keys = await fileSystem.getKeys(); + final protectedKeys = {}; + if (contentHash != null) { + protectedKeys.add(documentStateContentKey(contentHash)); + } + if (pathKey != null) { + protectedKeys.add(pathKey); + } + + // Load all records once + final records = <({String key, PersistedDocumentState state})>[]; + for (final key in keys) { + final state = await _getFileOrNull(key); + if (state == null) continue; + records.add((key: key, state: state)); + } + + // Delete expired records + final cutoff = (now ?? DateTime.now().toUtc()).subtract( + Duration(days: settings.maxAgeDays), + ); + for (final record in records) { + final updatedAt = record.state.updatedAt; + if (protectedKeys.contains(record.key) || updatedAt == null) continue; + if (updatedAt.isBefore(cutoff)) { + await fileSystem.deleteFile(record.key); + } + } + + // Delete overflow records (oldest first) + final remaining = records + .where((r) => !protectedKeys.contains(r.key)) + .toList(); + if (remaining.length > settings.maxEntries) { + remaining.sort((a, b) { + final aTime = + a.state.updatedAt ?? DateTime.fromMillisecondsSinceEpoch(0); + final bTime = + b.state.updatedAt ?? DateTime.fromMillisecondsSinceEpoch(0); + return aTime.compareTo(bTime); + }); + final overflow = remaining.length - settings.maxEntries; + for (final record in remaining.take(overflow)) { + await fileSystem.deleteFile(record.key); + } + } + }); + + Future _updateFile( + String? oldKey, + String? newKey, PersistedDocumentState state, - DocumentStatePersistenceSettings settings, + bool persistentChanged, ) async { - final existing = await _getFileOrNull(key); - final next = _mergeEnabled(existing, state, settings); - if (existing != null || await fileSystem.hasKey(key)) { - await fileSystem.updateFile(key, next); + // No keys to update + if (oldKey == null && newKey == null) return; + + // Same key and no changes to persist + if (oldKey == newKey && !persistentChanged) return; + + final key = (newKey ?? oldKey)!; + final keyChanged = oldKey != newKey; + + if (!keyChanged) { + // Same key, only update if persistent data changed + if (persistentChanged) { + await fileSystem.updateFile(key, state); + } + } else if (persistentChanged || oldKey == null) { + // Key changed or new key: delete old and create new + if (oldKey != null) { + await fileSystem.deleteFile(oldKey); + } + await fileSystem.createFile(key, state); } else { - await fileSystem.createFile(key, next); + // Key changed but only transient data changed: rename + await fileSystem.renameFile(oldKey, key); } } @@ -142,40 +162,28 @@ class DocumentStateRepository { : const PersistedAreaNavigatorState(), ); - PersistedDocumentState _mergeEnabled( - PersistedDocumentState? existing, - PersistedDocumentState state, - DocumentStatePersistenceSettings settings, - ) { - final fallback = existing ?? const PersistedDocumentState(); - return state.copyWith( - pageName: settings.page ? state.pageName : fallback.pageName, - camera: settings.camera ? state.camera : fallback.camera, - locks: settings.locks ? state.locks : fallback.locks, - selectedTool: settings.tool ? state.selectedTool : fallback.selectedTool, - navigator: settings.navigator ? state.navigator : fallback.navigator, - layers: settings.layers ? state.layers : fallback.layers, - areaNavigator: settings.areas - ? state.areaNavigator - : fallback.areaNavigator, - ); - } - Future _getFileOrNull(String key) async { try { return await fileSystem.getFile(key); - } on FormatException { + } on FormatException catch (e) { + debugPrint('Failed to parse document state at $key: $e'); return null; } } + static const _minCleanupInterval = Duration(hours: 1); + void _scheduleCleanupAfterSave({String? contentHash, String? pathKey}) { - final now = DateTime.now().toUtc(); + // Skip if cleanup already scheduled or running + if (_scheduledCleanup != null || _cleanupStarted) return; + + // Skip if cleanup ran recently final last = _lastCleanupAt; - if (last != null && now.difference(last) < const Duration(minutes: 10)) { + if (last != null && + DateTime.now().toUtc().difference(last) < _minCleanupInterval) { return; } - if (_scheduledCleanup != null) return; + _scheduledCleanup = Future(() { return _cleanupAfterSave(contentHash: contentHash, pathKey: pathKey); diff --git a/app/lib/views/main.dart b/app/lib/views/main.dart index 7f83e3b51441..fe0300e6da86 100644 --- a/app/lib/views/main.dart +++ b/app/lib/views/main.dart @@ -451,7 +451,7 @@ class _ProjectPageState extends State { final isImportedDocument = documentOpened && !(location.fileType?.isNote() ?? false); if (!absolute && isImportedDocument) { - currentIndexCubit.setSaveState(saved: SaveState.unsaved); + editorController.saveCubit.setSaveState(saved: SaveState.unsaved); } networkingService.setup(bloc); setState(() { diff --git a/app/test/bloc/document_bloc_test.dart b/app/test/bloc/document_bloc_test.dart index e7b8789aaf52..03bdc62980a0 100644 --- a/app/test/bloc/document_bloc_test.dart +++ b/app/test/bloc/document_bloc_test.dart @@ -267,12 +267,16 @@ void main() { ); test('force saving an already saved document does not write again', () async { - expect(currentIndexCubit.state.saved, SaveState.saved); + expect(editorController.saveCubit.state.saved, SaveState.saved); - final location = await currentIndexCubit.save(bloc, force: true); + final location = await editorController.saveCubit.save( + bloc, + editorController.networkingService, + force: true, + ); expect(location, const AssetLocation(path: 'test-note.bfly')); - expect(currentIndexCubit.state.saved, SaveState.saved); + expect(editorController.saveCubit.state.saved, SaveState.saved); }); test('duplicating area adds it to selected pages', () async { diff --git a/app/test/cubits/editor_session_test.dart b/app/test/cubits/editor_session_test.dart index a3c5452c5dc8..467e3dfb375a 100644 --- a/app/test/cubits/editor_session_test.dart +++ b/app/test/cubits/editor_session_test.dart @@ -66,7 +66,7 @@ void main() { fileSystem = buildMockDocumentStateFileSystem(); }); - test('content hash match wins over path match', () async { + test('path match wins over content hash match', () async { const byContent = PersistedDocumentState(pageName: 'Content Page'); const byPath = PersistedDocumentState(pageName: 'Path Page'); await fileSystem.initialize(); @@ -77,7 +77,7 @@ void main() { fileSystem, ).load(contentHash: 'hash-a', pathKey: 'path/a'); - expect(loaded?.pageName, 'Content Page'); + expect(loaded?.pageName, 'Path Page'); }); test('falls back to path when content hash is missing', () async { @@ -115,6 +115,7 @@ void main() { await repository.save( const PersistedDocumentState(pageName: 'Changed'), pathKey: 'path/b', + persistentChanged: true, ); expect(await fileSystem.getFile('path/b'), isNull); }); @@ -157,6 +158,7 @@ void main() { locks: PersistentLockState(lockLayer: true), ), pathKey: 'path/a', + persistentChanged: true, ); final saved = await fileSystem.getFile('path/a'); @@ -211,12 +213,6 @@ void main() { cubit.updateNavigator(enabled: true, page: NavigatorPage.layers); await cubit.saveNow(); - expect( - (await fileSystem.getFile( - documentStateContentKey('hash-a'), - ))?.navigator.page, - NavigatorPage.layers.name, - ); final contentRecord = await fileSystem.getFile( documentStateContentKey('hash-a'), ); @@ -231,6 +227,83 @@ void main() { await transformCubit.close(); }); + test('writes session state to updated document identity', () async { + final transformCubit = TransformCubit(1); + final cubit = EditorSessionCubit( + repository: DocumentStateRepository(fileSystem), + transformCubit: transformCubit, + initialState: const PersistedDocumentState(pageName: 'Page 1'), + pathKey: 'path/old', + contentHash: 'hash-old', + ); + + await cubit.saveNow(pathKey: 'path/new'); + + expect(await fileSystem.getFile('path/old'), isNull); + final pathRecord = await fileSystem.getFile('path/new'); + final contentRecord = await fileSystem.getFile( + documentStateContentKey('hash-old'), + ); + expect(pathRecord?.pathKey, 'path/old'); + expect(pathRecord?.contentHash, 'hash-old'); + expect(contentRecord?.pathKey, 'path/old'); + expect(contentRecord?.contentHash, 'hash-old'); + + await cubit.close(); + await transformCubit.close(); + }); + + test('rewrites state when document identity and state changed', () async { + final transformCubit = TransformCubit(1); + final cubit = EditorSessionCubit( + repository: DocumentStateRepository(fileSystem), + transformCubit: transformCubit, + initialState: const PersistedDocumentState(pageName: 'Page 1'), + pathKey: 'path/old', + contentHash: 'hash-old', + ); + + cubit.updateNavigator(enabled: true); + await cubit.saveNow(pathKey: 'path/new'); + + expect(await fileSystem.getFile('path/old'), isNull); + final pathRecord = await fileSystem.getFile('path/new'); + expect(pathRecord?.pathKey, 'path/new'); + expect(pathRecord?.navigator.enabled, isTrue); + + await cubit.close(); + await transformCubit.close(); + }); + + test('renames content identity without leaving old content key', () async { + final transformCubit = TransformCubit(1); + final cubit = EditorSessionCubit( + repository: DocumentStateRepository(fileSystem), + transformCubit: transformCubit, + initialState: const PersistedDocumentState(pageName: 'Page 1'), + pathKey: 'path/a', + contentHash: 'hash-old', + ); + + cubit.updateNavigator(enabled: true); + await cubit.saveNow(); + await cubit.saveNow(contentHash: 'hash-new'); + + expect( + await fileSystem.getFile(documentStateContentKey('hash-old')), + isNull, + ); + final contentRecord = await fileSystem.getFile( + documentStateContentKey('hash-new'), + ); + final pathRecord = await fileSystem.getFile('path/a'); + expect(contentRecord?.contentHash, 'hash-new'); + expect(pathRecord?.contentHash, 'hash-new'); + + await cubit.close(); + await transformCubit.close(); + }); + test('camera changes stay in memory until session is flushed', () async { final transformCubit = TransformCubit(1); final cubit = EditorSessionCubit( From 1dadf2cb4e223886c9411af52fec1be95230cd12 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Fri, 3 Jul 2026 21:24:51 +0200 Subject: [PATCH 041/117] Improve performance of saving and rendering --- app/lib/cubits/document_save.dart | 17 +-- app/lib/cubits/editor_renderer.dart | 48 ++++---- app/lib/cubits/editor_runtime.dart | 5 +- app/lib/view_painter.dart | 103 +++++++++++++----- app/test/bloc/document_bloc_test.dart | 5 + .../views/project_page_lifecycle_test.dart | 14 +-- 6 files changed, 124 insertions(+), 68 deletions(-) diff --git a/app/lib/cubits/document_save.dart b/app/lib/cubits/document_save.dart index b02dab35901c..274db8dac64b 100644 --- a/app/lib/cubits/document_save.dart +++ b/app/lib/cubits/document_save.dart @@ -118,9 +118,12 @@ class DocumentSaveCubit extends Cubit { setSaveState(saved: SaveState.saved); return AssetLocation.empty; } - Uint8List currentDataBytes; + String contentHash; if (absolute || !(current.fileType?.isNote() ?? false)) { - final file = await compute(_toFile, (currentData, false)); + final (file, hash) = await compute(_toFileWithContentHash, ( + currentData, + false, + )); final document = await fileSystem.createFileWithName( name: currentData.name, suffix: '.bfly', @@ -132,21 +135,19 @@ class DocumentSaveCubit extends Cubit { file, ); current = document.location; - currentDataBytes = file.data; + contentHash = hash; } else { - final file = await compute(_toFile, ( + final (file, hash) = await compute(_toFileWithContentHash, ( currentData, current.fileType == AssetFileType.textNote, )); await fileSystem.updateFile(current.path, file); - currentDataBytes = file.data; + contentHash = hash; } settingsCubit.addRecentHistory(current); await editorSessionCubit?.saveNow( pathKey: documentStatePathKeyOrNull(current), - contentHash: documentStateContentKey( - documentStateContentHash(currentDataBytes), - ), + contentHash: documentStateContentKey(contentHash), ); if (isClosed) { return current; diff --git a/app/lib/cubits/editor_renderer.dart b/app/lib/cubits/editor_renderer.dart index b9754910f190..5592c886b49c 100644 --- a/app/lib/cubits/editor_renderer.dart +++ b/app/lib/cubits/editor_renderer.dart @@ -363,7 +363,6 @@ class RendererCubit extends Cubit { size /= resolution.multiplier; } var transform = transformCubit.state; - var renderers = List>.from(rendererCubit.renderers); final recorder = ui.PictureRecorder(); final canvas = ui.Canvas(recorder); final rect = rendererCubit.getViewportRect( @@ -409,9 +408,9 @@ class RendererCubit extends Cubit { resetAllLayers = resetAllLayers || viewChanged; if (cameraViewport.unbakedElements.isEmpty && !reset) return; final currentLayer = blocState.currentLayer; + final renderers = rendererCubit.renderers; List> visibleElements; final oldVisible = cameraViewport.visibleElements; - final oldVisibleSet = oldVisible.toSet(); talker.verbose( 'Baking viewport (reset: $reset, viewChanged: $viewChanged, ' 'rendererStatesChanged: $rendererStatesChanged)', @@ -422,6 +421,7 @@ class RendererCubit extends Cubit { .where((renderer) => renderer.isVisible(rect)) .toList(); } else { + final oldVisibleSet = oldVisible.toSet(); visibleElements = List.from(oldVisible) ..addAll( cameraViewport.unbakedElements.where( @@ -431,8 +431,6 @@ class RendererCubit extends Cubit { ); } - final visibleElementsSet = visibleElements.toSet(); - await rendererCubit.updateOnVisible( controller, cameraViewport.unbake(visibleElements: visibleElements), @@ -557,24 +555,26 @@ class RendererCubit extends Cubit { } } - final bakedElementsSet = cameraViewport.bakedElements - .map((e) => e.element) - .toSet(); - final unbakedElementsSet = cameraViewport.unbakedElements - .map((e) => e.element) - .toSet(); - - final newlyUnbaked = - (reset - ? rendererCubit.renderers - : rendererCubit.state.cameraViewport.unbakedElements) - .where( - (element) => - !bakedElementsSet.contains(element.element) && - !unbakedElementsSet.contains(element.element) && - !visibleElementsSet.contains(element), - ) - .toList(); + final List> newlyUnbaked; + if (reset) { + final bakedElementsSet = cameraViewport.bakedElements + .map((e) => e.element) + .toSet(); + final unbakedElementsSet = cameraViewport.unbakedElements + .map((e) => e.element) + .toSet(); + final visibleElementsSet = visibleElements.toSet(); + newlyUnbaked = renderers + .where( + (element) => + !bakedElementsSet.contains(element.element) && + !unbakedElementsSet.contains(element.element) && + !visibleElementsSet.contains(element), + ) + .toList(); + } else { + newlyUnbaked = const []; + } if (controller.isClosed) return; @@ -620,9 +620,7 @@ class RendererCubit extends Cubit { bakedElements: renderers, unbakedElements: newlyUnbaked, visibleElements: visibleElements, - visibleUnbakedElements: newlyUnbaked - .where((renderer) => renderer.isVisible(rect)) - .toList(), + visibleUnbakedElements: const [], belowLayerImage: belowLayerImage, aboveLayerImage: aboveLayerImage, rendererStates: allRendererStates, diff --git a/app/lib/cubits/editor_runtime.dart b/app/lib/cubits/editor_runtime.dart index bc5db14128fd..bf20064f8a15 100644 --- a/app/lib/cubits/editor_runtime.dart +++ b/app/lib/cubits/editor_runtime.dart @@ -40,8 +40,9 @@ part 'editor_input.dart'; part 'document_save.dart'; part 'editor_view.dart'; -Future _toFile((NoteData, bool) args) async { - return args.$1.toFile(isTextBased: args.$2); +Future<(NoteFile, String)> _toFileWithContentHash((NoteData, bool) args) async { + final file = args.$1.toFile(isTextBased: args.$2); + return (file, documentStateContentHash(file.data)); } void _sendNetworkingState( diff --git a/app/lib/view_painter.dart b/app/lib/view_painter.dart index a785260bae64..b4cab90feeb5 100644 --- a/app/lib/view_painter.dart +++ b/app/lib/view_painter.dart @@ -7,7 +7,6 @@ import 'package:butterfly/models/viewport.dart'; import 'package:butterfly/renderers/renderer.dart'; import 'package:butterfly/views/navigator/constants.dart'; import 'package:butterfly_api/butterfly_api.dart'; -import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:material_leap/material_leap.dart'; @@ -27,14 +26,17 @@ void _paintRenderer( bool foreground = false, bool combined = false, }) { - canvas.save(); - final center = renderer.rect?.center; - if (center != null) { - canvas.translate(center.dx, center.dy); - } - canvas.rotate(renderer.rotation * (pi / 180)); - if (center != null) { - canvas.translate(-center.dx, -center.dy); + final rotation = renderer.rotation; + if (rotation != 0) { + canvas.save(); + final center = renderer.rect?.center; + if (center != null) { + canvas.translate(center.dx, center.dy); + } + canvas.rotate(rotation * (pi / 180)); + if (center != null) { + canvas.translate(-center.dx, -center.dy); + } } if (combined && renderer is PenRenderer) { renderer.buildCombined(canvas); @@ -50,7 +52,21 @@ void _paintRenderer( foreground, ); } - canvas.restore(); + if (rotation != 0) { + canvas.restore(); + } +} + +Rect? _rendererBounds(Renderer renderer) => renderer.expandedRect; + +Rect? _groupBounds(Iterable renderers) { + Rect? bounds; + for (final renderer in renderers) { + final rect = _rendererBounds(renderer); + if (rect == null) return null; + bounds = bounds?.expandToInclude(rect) ?? rect; + } + return bounds; } void _paintRenderers( @@ -64,13 +80,31 @@ void _paintRenderers( Iterable renderers, { bool foreground = false, }) { - final rendererList = renderers.toList(); - final groups = >{}; + final rendererList = renderers is List + ? renderers + : renderers.toList(); + Map>? groups; for (final renderer in rendererList.whereType()) { final combineId = renderer.element.combineId; if (combineId != null) { - groups.putIfAbsent(combineId, () => []).add(renderer); + (groups ??= {}).putIfAbsent(combineId, () => []).add(renderer); + } + } + if (groups == null) { + for (final renderer in rendererList) { + _paintRenderer( + canvas, + size, + document, + page, + info, + transform, + colorScheme, + renderer, + foreground: foreground, + ); } + return; } final paintedGroups = {}; for (final renderer in rendererList) { @@ -92,8 +126,24 @@ void _paintRenderers( continue; } if (!paintedGroups.add(combineId)) continue; - canvas.saveLayer(null, Paint()); - for (final groupedRenderer in groups[combineId]!) { + final group = groups[combineId]!; + if (group.length == 1) { + _paintRenderer( + canvas, + size, + document, + page, + info, + transform, + colorScheme, + group.first, + foreground: foreground, + combined: true, + ); + continue; + } + canvas.saveLayer(_groupBounds(group), Paint()); + for (final groupedRenderer in group) { _paintRenderer( canvas, size, @@ -244,13 +294,16 @@ class ViewPainter extends CustomPainter { } final areaSelectionWidth = 5 * transform.size; if (areaRect != null) { + final visibleRect = + transform.position & (Size(size.width, size.height) / transform.size); final currentAreaColor = currentArea?.color?.toColor(); final paint = Paint() ..style = PaintingStyle.stroke ..color = currentAreaColor ?? colorScheme?.primary ?? Colors.green ..strokeWidth = areaSelectionWidth; canvas.drawRect(areaRect.inflate(areaSelectionWidth / 2), paint); - for (final area in page.areas.sortedBy((a) => a == currentArea ? 1 : 0)) { + for (final area in page.areas) { + if (area == currentArea || !area.rect.overlaps(visibleRect)) continue; if (areaRect.overlaps(area.rect)) continue; var rect = area.rect; rect = Rect.fromPoints( @@ -262,10 +315,7 @@ class ViewPainter extends CustomPainter { ..color = area.color?.toColor() ?? colorScheme?.secondary ?? Colors.grey ..strokeWidth = areaSelectionWidth; - canvas.drawRect( - rect.inflate(areaSelectionWidth / 2), - area == currentArea ? paint : areaPaint, - ); + canvas.drawRect(rect.inflate(areaSelectionWidth / 2), areaPaint); } canvas.clipRect(areaRect); } @@ -322,16 +372,17 @@ class ViewPainter extends CustomPainter { @override bool shouldRepaint(ViewPainter oldDelegate) { final shouldRepaint = - document != oldDelegate.document || - page != oldDelegate.page || - info != oldDelegate.info || + !identical(document, oldDelegate.document) || + !identical(page, oldDelegate.page) || + !identical(info, oldDelegate.info) || renderBackground != oldDelegate.renderBackground || renderBaked != oldDelegate.renderBaked || renderBakedLayers != oldDelegate.renderBakedLayers || transform != oldDelegate.transform || - cameraViewport != oldDelegate.cameraViewport || - !setEquals(invisibleLayers, oldDelegate.invisibleLayers) || - currentArea != oldDelegate.currentArea || + !identical(cameraViewport, oldDelegate.cameraViewport) || + (!identical(invisibleLayers, oldDelegate.invisibleLayers) && + !setEquals(invisibleLayers, oldDelegate.invisibleLayers)) || + !identical(currentArea, oldDelegate.currentArea) || colorScheme != oldDelegate.colorScheme; return shouldRepaint; } diff --git a/app/test/bloc/document_bloc_test.dart b/app/test/bloc/document_bloc_test.dart index 03bdc62980a0..9e42651bd91a 100644 --- a/app/test/bloc/document_bloc_test.dart +++ b/app/test/bloc/document_bloc_test.dart @@ -175,6 +175,10 @@ void main() { late WindowCubit windowCubit; late DocumentBloc bloc; + setUpAll(() { + registerFallbackValue(const AssetLocation(path: 'fallback.bfly')); + }); + setUp(() { fileSystem = MockButterflyFileSystem(); settingsCubit = fileSystem.settingsCubit as MockSettingsCubit; @@ -183,6 +187,7 @@ void main() { () => settingsCubit.state, ).thenReturn(const ButterflySettings(autosave: false)); when(() => settingsCubit.stream).thenAnswer((_) => const Stream.empty()); + when(() => settingsCubit.addRecentHistory(any())).thenAnswer((_) async {}); editorController = EditorController( settingsCubit, diff --git a/app/test/views/project_page_lifecycle_test.dart b/app/test/views/project_page_lifecycle_test.dart index e23649961626..e9f3b4e0e9b5 100644 --- a/app/test/views/project_page_lifecycle_test.dart +++ b/app/test/views/project_page_lifecycle_test.dart @@ -197,13 +197,13 @@ void main() { await tester.pumpWidget(buildApp()); router.go('/import'); - await pumpUntil( - tester, - () => - find.byType(ProjectPage).evaluate().isNotEmpty && - observer.lastSaveCubit != null, - 'imported document open', - ); + await pumpUntil( + tester, + () => + find.byType(ProjectPage).evaluate().isNotEmpty && + observer.lastSaveCubit != null, + 'imported document open', + ); expect(observer.lastSaveCubit!.state.saved, SaveState.unsaved); From 3f81ff3aef36b4b4bde07ae907b810b94d2ada0c Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sat, 4 Jul 2026 10:47:45 +0200 Subject: [PATCH 042/117] Fix pathkey and contenthash being saved, fix state not saving before close --- app/lib/cubits/document_save.dart | 2 +- app/lib/cubits/editor_session.dart | 22 ++++++-- app/lib/models/persisted_document_state.dart | 4 +- .../persisted_document_state.freezed.dart | 12 ++--- .../models/persisted_document_state.g.dart | 4 -- app/lib/repositories/document_state.dart | 47 +++++++++++++++-- app/lib/settings/behaviors/persistence.dart | 30 +++++++++++ app/lib/views/main.dart | 15 ++++-- app/test/cubits/editor_session_test.dart | 50 ++++++++++++++----- 9 files changed, 151 insertions(+), 35 deletions(-) diff --git a/app/lib/cubits/document_save.dart b/app/lib/cubits/document_save.dart index 274db8dac64b..f06733a26420 100644 --- a/app/lib/cubits/document_save.dart +++ b/app/lib/cubits/document_save.dart @@ -147,7 +147,7 @@ class DocumentSaveCubit extends Cubit { settingsCubit.addRecentHistory(current); await editorSessionCubit?.saveNow( pathKey: documentStatePathKeyOrNull(current), - contentHash: documentStateContentKey(contentHash), + contentHash: contentHash, ); if (isClosed) { return current; diff --git a/app/lib/cubits/editor_session.dart b/app/lib/cubits/editor_session.dart index 5f821bcdf64a..f9a9e997bd79 100644 --- a/app/lib/cubits/editor_session.dart +++ b/app/lib/cubits/editor_session.dart @@ -17,7 +17,12 @@ class EditorSessionCubit extends Cubit { String? pathKey, String? contentHash, }) : _transformCubit = transformCubit, - super(initialState) { + super( + initialState.copyWith( + pathKey: pathKey ?? initialState.pathKey, + contentHash: contentHash ?? initialState.contentHash, + ), + ) { _transformSubscription = transformCubit.stream.listen(_onTransformChanged); } @@ -34,6 +39,8 @@ class EditorSessionCubit extends Cubit { _pendingSave; var _dirty = false; + bool get isDirty => _dirty || _pendingSave != null || _saveDebounce != null; + static PersistedDocumentState buildInitial({ PersistedDocumentState? restored, required NoteData document, @@ -217,9 +224,13 @@ class EditorSessionCubit extends Cubit { try { await repository.save( next, - contentKey: next.contentHash, + contentKey: next.contentHash == null + ? null + : documentStateContentKey(next.contentHash!), pathKey: next.pathKey, - previousContentKey: previousState?.contentHash, + previousContentKey: previousState?.contentHash == null + ? null + : documentStateContentKey(previousState!.contentHash!), previousPathKey: previousState?.pathKey, persistentChanged: pending.persistentChanged, ); @@ -240,6 +251,11 @@ class EditorSessionCubit extends Cubit { Future close() async { _saveDebounce?.cancel(); _saveDebounce = null; + if (_dirty || _pendingSave != null) { + await saveNow(); + } else { + await _saveFuture; + } await _transformSubscription?.cancel(); return super.close(); } diff --git a/app/lib/models/persisted_document_state.dart b/app/lib/models/persisted_document_state.dart index ca8aa85513dc..736d09244be5 100644 --- a/app/lib/models/persisted_document_state.dart +++ b/app/lib/models/persisted_document_state.dart @@ -149,8 +149,8 @@ sealed class PersistedDocumentState with _$PersistedDocumentState { const factory PersistedDocumentState({ @Default(kPersistedDocumentStateVersion) int version, - String? pathKey, - String? contentHash, + @JsonKey(includeFromJson: false, includeToJson: false) String? pathKey, + @JsonKey(includeFromJson: false, includeToJson: false) String? contentHash, String? pageName, @Default(PersistedCameraState()) PersistedCameraState camera, @JsonKey(readValue: _readLocks) diff --git a/app/lib/models/persisted_document_state.freezed.dart b/app/lib/models/persisted_document_state.freezed.dart index 18c4e5712b50..fdaf16b0b72f 100644 --- a/app/lib/models/persisted_document_state.freezed.dart +++ b/app/lib/models/persisted_document_state.freezed.dart @@ -861,7 +861,7 @@ as bool, /// @nodoc mixin _$PersistedDocumentState { - int get version; String? get pathKey; String? get contentHash; String? get pageName; PersistedCameraState get camera;@JsonKey(readValue: _readLocks) PersistentLockState get locks; PersistedToolSelection get selectedTool;@JsonKey(readValue: _readNavigator) PersistedNavigatorState get navigator;@JsonKey(readValue: _readLayers) PersistedLayerState get layers;@JsonKey(readValue: _readAreaNavigator) PersistedAreaNavigatorState get areaNavigator; DateTime? get updatedAt; + int get version;@JsonKey(includeFromJson: false, includeToJson: false) String? get pathKey;@JsonKey(includeFromJson: false, includeToJson: false) String? get contentHash; String? get pageName; PersistedCameraState get camera;@JsonKey(readValue: _readLocks) PersistentLockState get locks; PersistedToolSelection get selectedTool;@JsonKey(readValue: _readNavigator) PersistedNavigatorState get navigator;@JsonKey(readValue: _readLayers) PersistedLayerState get layers;@JsonKey(readValue: _readAreaNavigator) PersistedAreaNavigatorState get areaNavigator; DateTime? get updatedAt; /// Create a copy of PersistedDocumentState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -894,7 +894,7 @@ abstract mixin class $PersistedDocumentStateCopyWith<$Res> { factory $PersistedDocumentStateCopyWith(PersistedDocumentState value, $Res Function(PersistedDocumentState) _then) = _$PersistedDocumentStateCopyWithImpl; @useResult $Res call({ - int version, String? pathKey, String? contentHash, String? pageName, PersistedCameraState camera,@JsonKey(readValue: _readLocks) PersistentLockState locks, PersistedToolSelection selectedTool,@JsonKey(readValue: _readNavigator) PersistedNavigatorState navigator,@JsonKey(readValue: _readLayers) PersistedLayerState layers,@JsonKey(readValue: _readAreaNavigator) PersistedAreaNavigatorState areaNavigator, DateTime? updatedAt + int version,@JsonKey(includeFromJson: false, includeToJson: false) String? pathKey,@JsonKey(includeFromJson: false, includeToJson: false) String? contentHash, String? pageName, PersistedCameraState camera,@JsonKey(readValue: _readLocks) PersistentLockState locks, PersistedToolSelection selectedTool,@JsonKey(readValue: _readNavigator) PersistedNavigatorState navigator,@JsonKey(readValue: _readLayers) PersistedLayerState layers,@JsonKey(readValue: _readAreaNavigator) PersistedAreaNavigatorState areaNavigator, DateTime? updatedAt }); @@ -990,12 +990,12 @@ $PersistedAreaNavigatorStateCopyWith<$Res> get areaNavigator { @JsonSerializable() class _PersistedDocumentState extends PersistedDocumentState { - const _PersistedDocumentState({this.version = kPersistedDocumentStateVersion, this.pathKey, this.contentHash, this.pageName, this.camera = const PersistedCameraState(), @JsonKey(readValue: _readLocks) this.locks = const PersistentLockState(), this.selectedTool = const PersistedToolSelection(), @JsonKey(readValue: _readNavigator) this.navigator = const PersistedNavigatorState(), @JsonKey(readValue: _readLayers) this.layers = const PersistedLayerState(), @JsonKey(readValue: _readAreaNavigator) this.areaNavigator = const PersistedAreaNavigatorState(), this.updatedAt}): super._(); + const _PersistedDocumentState({this.version = kPersistedDocumentStateVersion, @JsonKey(includeFromJson: false, includeToJson: false) this.pathKey, @JsonKey(includeFromJson: false, includeToJson: false) this.contentHash, this.pageName, this.camera = const PersistedCameraState(), @JsonKey(readValue: _readLocks) this.locks = const PersistentLockState(), this.selectedTool = const PersistedToolSelection(), @JsonKey(readValue: _readNavigator) this.navigator = const PersistedNavigatorState(), @JsonKey(readValue: _readLayers) this.layers = const PersistedLayerState(), @JsonKey(readValue: _readAreaNavigator) this.areaNavigator = const PersistedAreaNavigatorState(), this.updatedAt}): super._(); factory _PersistedDocumentState.fromJson(Map json) => _$PersistedDocumentStateFromJson(json); @override@JsonKey() final int version; -@override final String? pathKey; -@override final String? contentHash; +@override@JsonKey(includeFromJson: false, includeToJson: false) final String? pathKey; +@override@JsonKey(includeFromJson: false, includeToJson: false) final String? contentHash; @override final String? pageName; @override@JsonKey() final PersistedCameraState camera; @override@JsonKey(readValue: _readLocks) final PersistentLockState locks; @@ -1038,7 +1038,7 @@ abstract mixin class _$PersistedDocumentStateCopyWith<$Res> implements $Persiste factory _$PersistedDocumentStateCopyWith(_PersistedDocumentState value, $Res Function(_PersistedDocumentState) _then) = __$PersistedDocumentStateCopyWithImpl; @override @useResult $Res call({ - int version, String? pathKey, String? contentHash, String? pageName, PersistedCameraState camera,@JsonKey(readValue: _readLocks) PersistentLockState locks, PersistedToolSelection selectedTool,@JsonKey(readValue: _readNavigator) PersistedNavigatorState navigator,@JsonKey(readValue: _readLayers) PersistedLayerState layers,@JsonKey(readValue: _readAreaNavigator) PersistedAreaNavigatorState areaNavigator, DateTime? updatedAt + int version,@JsonKey(includeFromJson: false, includeToJson: false) String? pathKey,@JsonKey(includeFromJson: false, includeToJson: false) String? contentHash, String? pageName, PersistedCameraState camera,@JsonKey(readValue: _readLocks) PersistentLockState locks, PersistedToolSelection selectedTool,@JsonKey(readValue: _readNavigator) PersistedNavigatorState navigator,@JsonKey(readValue: _readLayers) PersistedLayerState layers,@JsonKey(readValue: _readAreaNavigator) PersistedAreaNavigatorState areaNavigator, DateTime? updatedAt }); diff --git a/app/lib/models/persisted_document_state.g.dart b/app/lib/models/persisted_document_state.g.dart index a0b8fe5daaa0..e27aa3c29b0f 100644 --- a/app/lib/models/persisted_document_state.g.dart +++ b/app/lib/models/persisted_document_state.g.dart @@ -101,8 +101,6 @@ _PersistedDocumentState _$PersistedDocumentStateFromJson( Map json, ) => _PersistedDocumentState( version: (json['version'] as num?)?.toInt() ?? kPersistedDocumentStateVersion, - pathKey: json['pathKey'] as String?, - contentHash: json['contentHash'] as String?, pageName: json['pageName'] as String?, camera: json['camera'] == null ? const PersistedCameraState() @@ -145,8 +143,6 @@ Map _$PersistedDocumentStateToJson( _PersistedDocumentState instance, ) => { 'version': instance.version, - 'pathKey': instance.pathKey, - 'contentHash': instance.contentHash, 'pageName': instance.pageName, 'camera': instance.camera.toJson(), 'locks': instance.locks.toJson(), diff --git a/app/lib/repositories/document_state.dart b/app/lib/repositories/document_state.dart index a5a415ff4620..5033b4eb66df 100644 --- a/app/lib/repositories/document_state.dart +++ b/app/lib/repositories/document_state.dart @@ -111,6 +111,14 @@ class DocumentStateRepository { } }); + Future cleanupAll() => _lock.synchronized(() async { + await fileSystem.initialize(); + final keys = await fileSystem.getKeys(); + for (final key in keys) { + await fileSystem.deleteFile(key); + } + }); + Future _updateFile( String? oldKey, String? newKey, @@ -126,23 +134,56 @@ class DocumentStateRepository { final key = (newKey ?? oldKey)!; final keyChanged = oldKey != newKey; + final nextState = await _mergeDisabledSettings(oldKey ?? key, state); + if (!keyChanged) { // Same key, only update if persistent data changed if (persistentChanged) { - await fileSystem.updateFile(key, state); + await fileSystem.updateFile(key, nextState); } } else if (persistentChanged || oldKey == null) { // Key changed or new key: delete old and create new if (oldKey != null) { await fileSystem.deleteFile(oldKey); + await fileSystem.createFile(key, nextState); + } else { + await fileSystem.updateFile(key, nextState); } - await fileSystem.createFile(key, state); } else { - // Key changed but only transient data changed: rename + // Key changed but persisted data did not. await fileSystem.renameFile(oldKey, key); } } + Future _mergeDisabledSettings( + String key, + PersistedDocumentState state, + ) async { + final settings = _settings; + if (settings.page && + settings.camera && + settings.locks && + settings.tool && + settings.navigator && + settings.layers && + settings.areas) { + return state; + } + final existing = await _getFileOrNull(key); + if (existing == null) return state; + return state.copyWith( + pageName: settings.page ? state.pageName : existing.pageName, + camera: settings.camera ? state.camera : existing.camera, + locks: settings.locks ? state.locks : existing.locks, + selectedTool: settings.tool ? state.selectedTool : existing.selectedTool, + navigator: settings.navigator ? state.navigator : existing.navigator, + layers: settings.layers ? state.layers : existing.layers, + areaNavigator: settings.areas + ? state.areaNavigator + : existing.areaNavigator, + ); + } + PersistedDocumentState _applySettings( PersistedDocumentState state, DocumentStatePersistenceSettings settings, diff --git a/app/lib/settings/behaviors/persistence.dart b/app/lib/settings/behaviors/persistence.dart index abdd3fd9f1ea..9f6797bf8517 100644 --- a/app/lib/settings/behaviors/persistence.dart +++ b/app/lib/settings/behaviors/persistence.dart @@ -1,3 +1,5 @@ +import 'package:butterfly/api/file_system.dart'; +import 'package:butterfly/repositories/document_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; @@ -119,6 +121,17 @@ class PersistenceBehaviorSettings extends StatelessWidget { onChangeEnd: (value) => change(settings.copyWith(maxAgeDays: value.toInt())), ), + ListTile( + leading: const PhosphorIcon(PhosphorIconsLight.trash), + title: const Text('Clean up stored states'), + subtitle: const Text( + 'Delete records older than the limits above', + ), + enabled: settings.enabled, + onTap: settings.enabled + ? () => _cleanupPersistentStates(context) + : null, + ), ], ); }, @@ -127,4 +140,21 @@ class PersistenceBehaviorSettings extends StatelessWidget { ), ); } + + Future _cleanupPersistentStates(BuildContext context) async { + final messenger = ScaffoldMessenger.of(context); + final settingsCubit = context.read(); + final fileSystem = context.read(); + final remotes = [null, ...settingsCubit.state.connections]; + for (final remote in remotes) { + final repository = DocumentStateRepository( + fileSystem.buildDocumentStateSystem(remote), + settingsProvider: () => settingsCubit.state.documentStatePersistence, + ); + await repository.cleanup(); + } + messenger.showSnackBar( + SnackBar(content: Text(AppLocalizations.of(context).deleted)), + ); + } } diff --git a/app/lib/views/main.dart b/app/lib/views/main.dart index fe0300e6da86..ac061d518a49 100644 --- a/app/lib/views/main.dart +++ b/app/lib/views/main.dart @@ -647,7 +647,8 @@ class _ProjectPageState extends State { CloseRequest? _preventClose() { final saveState = _runtime?.editorController.saveCubit.state; - return saveState?.saved == SaveState.saved + final sessionDirty = _runtime?.editorSessionCubit?.isDirty ?? false; + return saveState?.saved == SaveState.saved && !sessionDirty ? null : CloseRequest( message: AppLocalizations.of(context).thereAreUnsavedChanges, @@ -656,10 +657,16 @@ class _ProjectPageState extends State { } Future _saveBeforeClose() async { - final bloc = _runtime?.bloc; + final runtime = _runtime; + final bloc = runtime?.bloc; if (bloc == null || bloc.isClosed) return false; - await bloc.save(force: true); - return bloc.editorController.saveCubit.state.saved == SaveState.saved; + if (bloc.editorController.saveCubit.state.saved != SaveState.saved) { + await bloc.save(force: true); + } else { + await runtime?.editorSessionCubit?.saveNow(); + } + return bloc.editorController.saveCubit.state.saved == SaveState.saved && + !(runtime?.editorSessionCubit?.isDirty ?? false); } Map> _buildActions(BuildContext context) { diff --git a/app/test/cubits/editor_session_test.dart b/app/test/cubits/editor_session_test.dart index 467e3dfb375a..48e909a6b00c 100644 --- a/app/test/cubits/editor_session_test.dart +++ b/app/test/cubits/editor_session_test.dart @@ -46,7 +46,7 @@ void main() { encodePersistedDocumentState(state), ); - expect(decoded, state); + expect(decoded, state.copyWith(pathKey: null, contentHash: null)); }); test('uses schema defaults for sparse json', () { @@ -217,10 +217,10 @@ void main() { documentStateContentKey('hash-a'), ); final pathRecord = await fileSystem.getFile('path/a'); - expect(contentRecord?.pathKey, 'path/a'); - expect(contentRecord?.contentHash, 'hash-a'); - expect(pathRecord?.pathKey, 'path/a'); - expect(pathRecord?.contentHash, 'hash-a'); + expect(contentRecord?.pathKey, isNull); + expect(contentRecord?.contentHash, isNull); + expect(pathRecord?.pathKey, isNull); + expect(pathRecord?.contentHash, isNull); expect(pathRecord?.navigator.enabled, isTrue); await cubit.close(); @@ -237,6 +237,8 @@ void main() { contentHash: 'hash-old', ); + cubit.updateNavigator(enabled: true); + await cubit.saveNow(); await cubit.saveNow(pathKey: 'path/new'); expect(await fileSystem.getFile('path/old'), isNull); @@ -244,10 +246,11 @@ void main() { final contentRecord = await fileSystem.getFile( documentStateContentKey('hash-old'), ); - expect(pathRecord?.pathKey, 'path/old'); - expect(pathRecord?.contentHash, 'hash-old'); - expect(contentRecord?.pathKey, 'path/old'); - expect(contentRecord?.contentHash, 'hash-old'); + expect(pathRecord?.pathKey, isNull); + expect(pathRecord?.contentHash, isNull); + expect(contentRecord?.pathKey, isNull); + expect(contentRecord?.contentHash, isNull); + expect(pathRecord?.navigator.enabled, isTrue); await cubit.close(); await transformCubit.close(); @@ -268,7 +271,7 @@ void main() { expect(await fileSystem.getFile('path/old'), isNull); final pathRecord = await fileSystem.getFile('path/new'); - expect(pathRecord?.pathKey, 'path/new'); + expect(pathRecord?.pathKey, isNull); expect(pathRecord?.navigator.enabled, isTrue); await cubit.close(); @@ -297,8 +300,9 @@ void main() { documentStateContentKey('hash-new'), ); final pathRecord = await fileSystem.getFile('path/a'); - expect(contentRecord?.contentHash, 'hash-new'); - expect(pathRecord?.contentHash, 'hash-new'); + expect(contentRecord?.contentHash, isNull); + expect(pathRecord?.contentHash, isNull); + expect(contentRecord?.navigator.enabled, isTrue); await cubit.close(); await transformCubit.close(); @@ -357,5 +361,27 @@ void main() { await cubit.close(); await transformCubit.close(); }); + + test('close flushes unsaved session state', () async { + final transformCubit = TransformCubit(1); + final cubit = EditorSessionCubit( + repository: DocumentStateRepository(fileSystem), + transformCubit: transformCubit, + initialState: const PersistedDocumentState(pageName: 'Page 1'), + pathKey: 'path/a', + contentHash: 'hash-a', + ); + + cubit.updateNavigator(enabled: true); + expect(cubit.isDirty, isTrue); + + await cubit.close(); + + final saved = await fileSystem.getFile('path/a'); + expect(saved?.navigator.enabled, isTrue); + expect(saved?.contentHash, isNull); + + await transformCubit.close(); + }); }); } From fbe55136596dd690b2ee52f81dadfed9adef04f5 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sat, 4 Jul 2026 11:06:00 +0200 Subject: [PATCH 043/117] Add localization to persistent settings and cleanup dialog --- app/lib/l10n/app_en.arb | 22 ++- app/lib/repositories/document_state.dart | 17 +- app/lib/settings/behaviors/persistence.dart | 187 ++++++++++++++++++-- 3 files changed, 203 insertions(+), 23 deletions(-) diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index b88a176cb1a2..c03bf076ca34 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -1372,5 +1372,25 @@ "addTool": "Add tool", "nextPage": "Next page", "previousPage": "Previous page", - "persistenceDocumentStates": "Persistent document states" + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Current page", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + } } diff --git a/app/lib/repositories/document_state.dart b/app/lib/repositories/document_state.dart index 5033b4eb66df..ecdb942095c5 100644 --- a/app/lib/repositories/document_state.dart +++ b/app/lib/repositories/document_state.dart @@ -59,12 +59,13 @@ class DocumentStateRepository { ); }); - Future cleanup({String? contentHash, String? pathKey, DateTime? now}) => + Future cleanup({String? contentHash, String? pathKey, DateTime? now}) => _lock.synchronized(() async { final settings = _settings; await fileSystem.initialize(); final keys = await fileSystem.getKeys(); final protectedKeys = {}; + final deletedKeys = {}; if (contentHash != null) { protectedKeys.add(documentStateContentKey(contentHash)); } @@ -89,12 +90,17 @@ class DocumentStateRepository { if (protectedKeys.contains(record.key) || updatedAt == null) continue; if (updatedAt.isBefore(cutoff)) { await fileSystem.deleteFile(record.key); + deletedKeys.add(record.key); } } // Delete overflow records (oldest first) final remaining = records - .where((r) => !protectedKeys.contains(r.key)) + .where( + (r) => + !protectedKeys.contains(r.key) && + !deletedKeys.contains(r.key), + ) .toList(); if (remaining.length > settings.maxEntries) { remaining.sort((a, b) { @@ -107,16 +113,19 @@ class DocumentStateRepository { final overflow = remaining.length - settings.maxEntries; for (final record in remaining.take(overflow)) { await fileSystem.deleteFile(record.key); + deletedKeys.add(record.key); } } + return deletedKeys.length; }); - Future cleanupAll() => _lock.synchronized(() async { + Future cleanupAll() => _lock.synchronized(() async { await fileSystem.initialize(); final keys = await fileSystem.getKeys(); for (final key in keys) { await fileSystem.deleteFile(key); } + return keys.length; }); Future _updateFile( @@ -226,7 +235,7 @@ class DocumentStateRepository { } _scheduledCleanup = - Future(() { + Future.microtask(() { return _cleanupAfterSave(contentHash: contentHash, pathKey: pathKey); }).whenComplete(() { _scheduledCleanup = null; diff --git a/app/lib/settings/behaviors/persistence.dart b/app/lib/settings/behaviors/persistence.dart index 9f6797bf8517..ec425165869d 100644 --- a/app/lib/settings/behaviors/persistence.dart +++ b/app/lib/settings/behaviors/persistence.dart @@ -3,6 +3,7 @@ import 'package:butterfly/repositories/document_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; +import 'package:lw_file_system/lw_file_system.dart'; import 'package:material_leap/material_leap.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; @@ -35,7 +36,9 @@ class PersistenceBehaviorSettings extends StatelessWidget { SwitchListTile( value: settings.enabled, secondary: const PhosphorIcon(PhosphorIconsLight.power), - title: const Text('Enable persistent document states'), + title: Text( + AppLocalizations.of(context).persistentStatesEnabled, + ), onChanged: (value) => change(settings.copyWith(enabled: value)), ), @@ -43,7 +46,9 @@ class PersistenceBehaviorSettings extends StatelessWidget { SwitchListTile( value: settings.page, secondary: const PhosphorIcon(PhosphorIconsLight.file), - title: const Text('Current page'), + title: Text( + AppLocalizations.of(context).persistentStateCurrentPage, + ), onChanged: settings.enabled ? (value) => change(settings.copyWith(page: value)) : null, @@ -53,7 +58,9 @@ class PersistenceBehaviorSettings extends StatelessWidget { secondary: const PhosphorIcon( PhosphorIconsLight.frameCorners, ), - title: const Text('Viewport position and zoom'), + title: Text( + AppLocalizations.of(context).persistentStateViewport, + ), onChanged: settings.enabled ? (value) => change(settings.copyWith(camera: value)) : null, @@ -69,7 +76,9 @@ class PersistenceBehaviorSettings extends StatelessWidget { SwitchListTile( value: settings.tool, secondary: const PhosphorIcon(PhosphorIconsLight.toolbox), - title: const Text('Selected tool'), + title: Text( + AppLocalizations.of(context).persistentStateSelectedTool, + ), onChanged: settings.enabled ? (value) => change(settings.copyWith(tool: value)) : null, @@ -100,7 +109,9 @@ class PersistenceBehaviorSettings extends StatelessWidget { ), const Divider(), ExactSlider( - header: const Text('Maximum stored records'), + header: Text( + AppLocalizations.of(context).persistentStateMaxRecords, + ), leading: const PhosphorIcon(PhosphorIconsLight.listNumbers), value: settings.maxEntries.toDouble(), min: 20, @@ -111,7 +122,11 @@ class PersistenceBehaviorSettings extends StatelessWidget { change(settings.copyWith(maxEntries: value.toInt())), ), ExactSlider( - header: const Text('Delete records older than days'), + header: Text( + AppLocalizations.of( + context, + ).persistentStateDeleteOlderThanDays, + ), leading: const PhosphorIcon(PhosphorIconsLight.calendar), value: settings.maxAgeDays.toDouble(), min: 7, @@ -123,9 +138,13 @@ class PersistenceBehaviorSettings extends StatelessWidget { ), ListTile( leading: const PhosphorIcon(PhosphorIconsLight.trash), - title: const Text('Clean up stored states'), - subtitle: const Text( - 'Delete records older than the limits above', + title: Text( + AppLocalizations.of(context).persistentStateCleanup, + ), + subtitle: Text( + AppLocalizations.of( + context, + ).persistentStateCleanupDescription, ), enabled: settings.enabled, onTap: settings.enabled @@ -145,16 +164,148 @@ class PersistenceBehaviorSettings extends StatelessWidget { final messenger = ScaffoldMessenger.of(context); final settingsCubit = context.read(); final fileSystem = context.read(); - final remotes = [null, ...settingsCubit.state.connections]; - for (final remote in remotes) { - final repository = DocumentStateRepository( - fileSystem.buildDocumentStateSystem(remote), - settingsProvider: () => settingsCubit.state.documentStatePersistence, - ); - await repository.cleanup(); - } + final result = await _showCleanupTargetsDialog( + context, + settingsCubit, + fileSystem, + ); + if (result == null) return; + if (!context.mounted) return; messenger.showSnackBar( - SnackBar(content: Text(AppLocalizations.of(context).deleted)), + SnackBar( + content: Text( + AppLocalizations.of(context).persistentStateCleanupFeedback( + result.targetCount, + result.deletedRecords, + ), + ), + ), + ); + } + + Future<_CleanupResult?> _showCleanupTargetsDialog( + BuildContext context, + SettingsCubit settingsCubit, + ButterflyFileSystem fileSystem, + ) async { + final settings = settingsCubit.state; + final targets = [ + _CleanupTarget( + id: '', + label: AppLocalizations.of(context).local, + storage: null, + ), + ...settings.connections.map( + (connection) => _CleanupTarget( + id: connection.identifier, + label: connection.identifier, + storage: connection, + ), + ), + ]; + final selected = targets.map((target) => target.id).toSet(); + var cleaning = false; + + return showDialog<_CleanupResult>( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setState) => AlertDialog( + title: Text(AppLocalizations.of(context).persistentStateCleanup), + scrollable: true, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (cleaning) ...[ + const LinearProgressIndicator(), + const SizedBox(height: 16), + ], + for (final target in targets) + CheckboxListTile( + value: selected.contains(target.id), + onChanged: cleaning + ? null + : (value) { + setState(() { + if (value ?? false) { + selected.add(target.id); + } else { + selected.remove(target.id); + } + }); + }, + secondary: PhosphorIcon( + target.storage == null + ? PhosphorIconsLight.house + : PhosphorIconsLight.cloud, + ), + title: Text(target.label), + controlAffinity: ListTileControlAffinity.leading, + ), + ], + ), + actions: [ + TextButton( + onPressed: cleaning ? null : () => Navigator.of(context).pop(), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), + ), + ElevatedButton( + onPressed: selected.isEmpty || cleaning + ? null + : () async { + final selectedTargets = targets + .where((target) => selected.contains(target.id)) + .toList(); + setState(() => cleaning = true); + var deleted = 0; + for (final target in selectedTargets) { + final repository = DocumentStateRepository( + fileSystem.buildDocumentStateSystem(target.storage), + settingsProvider: () => + settingsCubit.state.documentStatePersistence, + ); + deleted += await repository.cleanup(); + } + if (!context.mounted) return; + Navigator.of(context).pop( + _CleanupResult( + targetCount: selectedTargets.length, + deletedRecords: deleted, + ), + ); + }, + child: cleaning + ? Text( + AppLocalizations.of( + context, + ).persistentStateCleanupInProgress, + ) + : Text(AppLocalizations.of(context).delete), + ), + ], + ), + ), ); } } + +class _CleanupTarget { + const _CleanupTarget({ + required this.id, + required this.label, + required this.storage, + }); + + final String id; + final String label; + final ExternalStorage? storage; +} + +class _CleanupResult { + const _CleanupResult({ + required this.targetCount, + required this.deletedRecords, + }); + + final int targetCount; + final int deletedRecords; +} From 486961a81e54c70778f17ac12cb5276ec25aef6f Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 5 Jul 2026 17:17:00 +0200 Subject: [PATCH 044/117] Use settings_leap to generate settings pages --- app/lib/actions/settings.dart | 2 +- app/lib/main.dart | 121 +-- app/lib/settings/behaviors/home.dart | 376 -------- app/lib/settings/behaviors/persistence.dart | 311 ------- app/lib/settings/data.dart | 880 ++++++++---------- app/lib/settings/experiments.dart | 127 --- app/lib/settings/general.dart | 331 ------- app/lib/settings/home.dart | 284 +++--- app/lib/settings/inputs/home.dart | 232 ----- app/lib/settings/inputs/keyboard.dart | 76 +- app/lib/settings/inputs/mouse.dart | 430 ++++----- app/lib/settings/inputs/pen.dart | 705 +++++++------- app/lib/settings/inputs/touch.dart | 224 ++--- app/lib/settings/logs.dart | 256 ----- app/lib/settings/pages/behaviors/home.dart | 134 +++ .../settings/pages/behaviors/persistence.dart | 274 ++++++ app/lib/settings/{ => pages}/connections.dart | 256 +++-- app/lib/settings/pages/data.dart | 60 ++ app/lib/settings/pages/experiments.dart | 66 ++ app/lib/settings/pages/general.dart | 261 ++++++ app/lib/settings/pages/inputs.dart | 288 ++++++ app/lib/settings/pages/logs.dart | 277 ++++++ app/lib/settings/pages/personalization.dart | 177 ++++ app/lib/settings/pages/view.dart | 310 ++++++ app/lib/settings/personalization.dart | 298 ------ app/lib/settings/view.dart | 403 -------- app/pubspec.lock | 9 + app/pubspec.yaml | 5 + 28 files changed, 3165 insertions(+), 4008 deletions(-) delete mode 100644 app/lib/settings/behaviors/home.dart delete mode 100644 app/lib/settings/behaviors/persistence.dart delete mode 100644 app/lib/settings/experiments.dart delete mode 100644 app/lib/settings/general.dart delete mode 100644 app/lib/settings/inputs/home.dart delete mode 100644 app/lib/settings/logs.dart create mode 100644 app/lib/settings/pages/behaviors/home.dart create mode 100644 app/lib/settings/pages/behaviors/persistence.dart rename app/lib/settings/{ => pages}/connections.dart (80%) create mode 100644 app/lib/settings/pages/data.dart create mode 100644 app/lib/settings/pages/experiments.dart create mode 100644 app/lib/settings/pages/general.dart create mode 100644 app/lib/settings/pages/inputs.dart create mode 100644 app/lib/settings/pages/logs.dart create mode 100644 app/lib/settings/pages/personalization.dart create mode 100644 app/lib/settings/pages/view.dart delete mode 100644 app/lib/settings/personalization.dart delete mode 100644 app/lib/settings/view.dart diff --git a/app/lib/actions/settings.dart b/app/lib/actions/settings.dart index fe2c247c583c..51afad225858 100644 --- a/app/lib/actions/settings.dart +++ b/app/lib/actions/settings.dart @@ -37,7 +37,7 @@ Future openSettings(BuildContext context) => showGeneralDialog( clipBehavior: Clip.antiAlias, child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 800, maxWidth: 1000), - child: const SettingsPage(isDialog: true), + child: const SettingsPage(inView: true), ), ), ), diff --git a/app/lib/main.dart b/app/lib/main.dart index 8b9c3c681f15..b5e20e3a066f 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -5,11 +5,7 @@ import 'package:butterfly/api/close.dart'; import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/api/intent.dart'; import 'package:butterfly/services/sync.dart'; -import 'package:butterfly/settings/behaviors/home.dart'; -import 'package:butterfly/settings/behaviors/persistence.dart'; -import 'package:butterfly/settings/inputs/mouse.dart'; -import 'package:butterfly/settings/experiments.dart'; -import 'package:butterfly/settings/view.dart'; +import 'package:butterfly/settings/connection.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -22,6 +18,7 @@ import 'package:lw_file_system/lw_file_system.dart'; import 'package:lw_sysapi/lw_sysapi.dart'; import 'package:material_leap/material_leap.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:settings_leap/settings_leap.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; import 'package:flutter_localized_locales/flutter_localized_locales.dart'; @@ -30,22 +27,12 @@ import 'package:talker_flutter/talker_flutter.dart'; import 'cubits/settings.dart'; import 'embed/embedding.dart'; -import 'settings/inputs/home.dart'; -import 'settings/inputs/keyboard.dart'; -import 'settings/inputs/pen.dart'; -import 'settings/inputs/touch.dart'; -import 'settings/data.dart'; -import 'settings/general.dart'; import 'settings/home.dart'; -import 'settings/personalization.dart'; -import 'settings/connection.dart'; -import 'settings/connections.dart'; import 'setup.dart' if (dart.library.js_interop) 'setup_web.dart'; import 'theme.dart'; import 'views/error.dart'; import 'views/home/page.dart'; import 'views/main.dart'; -import 'settings/logs.dart'; import 'services/logger.dart'; const platform = MethodChannel('linwood.dev/butterfly'); @@ -144,6 +131,36 @@ class ButterflyApp extends StatelessWidget { this.debugShowCheckedModeBanner = true, }); + List _buildSettingsRoute( + SettingsLeapTree tree, + ) { + List buildEntries( + Map> entries, + ) { + return entries.entries.map((entry) { + final id = entry.key; + final page = entry.value; + final children = page.children; + return GoRoute( + path: id, + builder: (context, state) => SettingsDetailsPage(id: id), + routes: [ + ...buildEntries(children), + if (id == 'connections') + GoRoute( + path: ':id', + name: 'connection', + builder: (context, state) => + ConnectionSettingsPage(remote: state.pathParameters['id']!), + ), + ], + ); + }).toList(); + } + + return buildEntries(tree.pages); + } + late final GoRouter _router = GoRouter( initialLocation: initialLocation, initialExtra: initialExtra, @@ -162,79 +179,7 @@ class ButterflyApp extends StatelessWidget { GoRoute( path: 'settings', builder: (context, state) => const SettingsPage(), - routes: [ - GoRoute( - path: 'general', - builder: (context, state) => const GeneralSettingsPage(), - ), - GoRoute( - path: 'inputs', - builder: (context, state) => const InputsSettingsPage(), - routes: [ - GoRoute( - path: 'mouse', - builder: (context, state) => const MouseInputSettings(), - ), - GoRoute( - path: 'pen', - builder: (context, state) => const PenInputSettings(), - ), - GoRoute( - path: 'keyboard', - builder: (context, state) => const KeyboardInputSettings(), - ), - GoRoute( - path: 'touch', - builder: (context, state) => const TouchInputSettings(), - ), - ], - ), - GoRoute( - path: 'behaviors', - builder: (context, state) => const BehaviorsSettingsPage(), - routes: [ - GoRoute( - path: 'persistence', - builder: (context, state) => - const PersistenceBehaviorSettings(), - ), - ], - ), - GoRoute( - path: 'personalization', - builder: (context, state) => - const PersonalizationSettingsPage(), - ), - GoRoute( - path: 'view', - builder: (context, state) => const ViewSettingsPage(), - ), - GoRoute( - path: 'data', - builder: (context, state) => const DataSettingsPage(), - ), - GoRoute( - path: 'experiments', - builder: (context, state) => const ExperimentsSettingsPage(), - ), - GoRoute( - path: 'connections', - builder: (context, state) => const ConnectionsSettingsPage(), - routes: [ - GoRoute( - path: ':id', - name: 'connection', - builder: (context, state) => ConnectionSettingsPage( - remote: state.pathParameters['id']!, - ), - ), - ], - ), - GoRoute( - path: 'logs', - builder: (context, state) => const LogsSettingsPage(), - ), - ], + routes: _buildSettingsRoute(settingsTree), ), GoRoute( name: 'new', diff --git a/app/lib/settings/behaviors/home.dart b/app/lib/settings/behaviors/home.dart deleted file mode 100644 index 3b412519654f..000000000000 --- a/app/lib/settings/behaviors/home.dart +++ /dev/null @@ -1,376 +0,0 @@ -import 'package:butterfly/cubits/settings.dart'; -import 'package:butterfly/theme.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:butterfly/src/generated/i18n/app_localizations.dart'; -import 'package:go_router/go_router.dart'; -import 'package:material_leap/material_leap.dart'; -import 'package:phosphor_flutter/phosphor_flutter.dart'; - -class BehaviorsSettingsPage extends StatelessWidget { - final bool inView; - - const BehaviorsSettingsPage({super.key, this.inView = false}); - - String _getStartupBehaviorName(BuildContext context, StartupBehavior value) => - switch (value) { - StartupBehavior.openHomeScreen => AppLocalizations.of( - context, - ).homeScreen, - StartupBehavior.openLastNote => AppLocalizations.of(context).lastNote, - StartupBehavior.openNewNote => AppLocalizations.of(context).newNote, - }; - - String _getRenderResolutionName( - BuildContext context, - RenderResolution value, - ) => switch (value) { - RenderResolution.performance => AppLocalizations.of(context).performance, - RenderResolution.normal => AppLocalizations.of(context).normal, - RenderResolution.high => AppLocalizations.of(context).high, - }; - - String _getRenderResolutionDescription( - BuildContext context, - RenderResolution value, - ) => switch (value) { - RenderResolution.performance => AppLocalizations.of( - context, - ).performanceDescription, - RenderResolution.normal => AppLocalizations.of(context).normalDescription, - RenderResolution.high => AppLocalizations.of(context).highDescription, - }; - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: inView ? Colors.transparent : null, - appBar: WindowTitleBar( - title: Text(AppLocalizations.of(context).behaviors), - backgroundColor: inView ? Colors.transparent : null, - inView: inView, - ), - body: BlocBuilder( - builder: (context, state) { - return ListView( - children: [ - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ListTile( - title: Text(AppLocalizations.of(context).autosave), - leading: const PhosphorIcon( - PhosphorIconsLight.floppyDisk, - ), - subtitle: Text( - state.autosave - ? state.delayedAutosave - ? AppLocalizations.of(context).delay - : state.showSaveButton - ? AppLocalizations.of( - context, - ).yesButShowButtons - : AppLocalizations.of(context).yes - : AppLocalizations.of(context).no, - ), - onTap: () => _openAutosaveModal(context), - ), - if (state.autosave && state.delayedAutosave) - ExactSlider( - leading: const PhosphorIcon(PhosphorIconsLight.clock), - header: Text( - AppLocalizations.of(context).autosaveDelay, - ), - value: state.autosaveDelaySeconds.toDouble(), - min: 1, - max: 10, - defaultValue: 3, - fractionDigits: 0, - onChangeEnd: (value) => context - .read() - .changeAutosaveDelaySeconds(value.toInt()), - ), - ListTile( - title: Text(AppLocalizations.of(context).onStartup), - subtitle: Text( - _getStartupBehaviorName(context, state.onStartup), - ), - onTap: () => _openStartupModal(context), - leading: const Icon(PhosphorIconsLight.arrowFatLineUp), - ), - ListTile( - title: Text( - AppLocalizations.of( - context, - ).persistenceDocumentStates, - ), - subtitle: Text( - state.documentStatePersistence.enabled - ? 'Enabled' - : AppLocalizations.of(context).off, - ), - onTap: () => - context.push('/settings/behaviors/persistence'), - leading: const PhosphorIcon( - PhosphorIconsLight.database, - ), - ), - SwitchListTile( - value: state.startInFullScreen, - onChanged: (value) => context - .read() - .changeStartInFullScreen(value), - title: Text( - AppLocalizations.of(context).startInFullScreen, - ), - secondary: const PhosphorIcon( - PhosphorIconsLight.arrowsOut, - ), - ), - ListTile( - leading: const PhosphorIcon( - PhosphorIconsLight.appWindow, - ), - title: Text( - AppLocalizations.of(context).contentViewport, - ), - subtitle: Text( - state.limitViewportMultiplier == null - ? AppLocalizations.of(context).off - : '${state.limitViewportMultiplier}x', - ), - onTap: () => _openContentViewportModal(context), - ), - SwitchListTile( - value: state.limitViewportPositive, - onChanged: (value) => context - .read() - .changeLimitViewportPositive(value), - title: Text( - AppLocalizations.of( - context, - ).limitViewportToPositiveCoordinates, - ), - secondary: const PhosphorIcon( - PhosphorIconsLight.plusSquare, - ), - ), - ListTile( - title: Text( - AppLocalizations.of(context).renderResolution, - ), - subtitle: Text( - _getRenderResolutionName( - context, - state.renderResolution, - ), - ), - onTap: () => _openRenderResolutionModal(context), - leading: const Icon(PhosphorIconsLight.sparkle), - ), - SwitchListTile( - title: Text( - AppLocalizations.of( - context, - ).bringMovedElementsToFront, - ), - value: state.bringMovedElementsToFront, - secondary: const PhosphorIcon(PhosphorIconsLight.stack), - onChanged: (value) => context - .read() - .changeBringMovedElementsToFront(value), - ), - ], - ), - ), - ), - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: settingsCardTitlePadding, - child: Text( - AppLocalizations.of(context).import, - style: TextTheme.of(context).headlineSmall, - ), - ), - const SizedBox(height: 16), - SwitchListTile( - value: state.spreadPages, - secondary: const PhosphorIcon( - PhosphorIconsLight.arrowsOutSimple, - ), - title: Text(AppLocalizations.of(context).spreadToPages), - onChanged: (value) => context - .read() - .changeSpreadPages(value), - ), - ExactSlider( - header: Text(AppLocalizations.of(context).imageScale), - leading: const PhosphorIcon( - PhosphorIconsLight.frameCorners, - ), - value: state.imageScale * 100, - min: 0, - max: 100, - defaultValue: 50, - fractionDigits: 0, - onChangeEnd: (value) => context - .read() - .changeImageScale(value / 100), - ), - ], - ), - ), - ), - ], - ); - }, - ), - ); - } - - void _openContentViewportModal(BuildContext context) { - final cubit = context.read(); - var currentMultiplier = cubit.state.limitViewportMultiplier; - showLeapBottomSheet( - context: context, - titleBuilder: (context) => - Text(AppLocalizations.of(context).contentViewport), - childrenBuilder: (context) { - final options = [ - (null, AppLocalizations.of(context).off), - (1.0, '1x'), - (1.5, '1.5x'), - (2.0, '2x'), - (3.0, '3x'), - ]; - return options - .map( - (e) => ListTile( - title: Text(e.$2), - selected: currentMultiplier == e.$1, - onTap: () { - cubit.changeLimitViewportMultiplier(e.$1); - Navigator.of(context).pop(); - }, - ), - ) - .toList(); - }, - ); - } - - void _openStartupModal(BuildContext context) { - final cubit = context.read(); - final currentStartup = cubit.state.onStartup; - - showLeapBottomSheet( - context: context, - titleBuilder: (context) => Text(AppLocalizations.of(context).onStartup), - childrenBuilder: (context) { - void changeStartup(StartupBehavior behavior) { - cubit.changeStartupBehavior(behavior); - Navigator.of(context).pop(); - } - - return StartupBehavior.values - .map( - (e) => ListTile( - title: Text(_getStartupBehaviorName(context, e)), - leading: Icon(switch (e) { - StartupBehavior.openHomeScreen => PhosphorIconsLight.house, - StartupBehavior.openLastNote => - PhosphorIconsLight.arrowCounterClockwise, - StartupBehavior.openNewNote => PhosphorIconsLight.file, - }, textDirection: TextDirection.ltr), - selected: currentStartup == e, - onTap: () => changeStartup(e), - ), - ) - .toList(); - }, - ); - } - - void _openRenderResolutionModal(BuildContext context) { - final cubit = context.read(); - final currentResolution = cubit.state.renderResolution; - - showLeapBottomSheet( - context: context, - titleBuilder: (context) => - Text(AppLocalizations.of(context).renderResolution), - childrenBuilder: (context) { - void changeResolution(RenderResolution resolution) { - cubit.changeRenderResolution(resolution); - Navigator.of(context).pop(); - } - - return RenderResolution.values - .map( - (e) => ListTile( - title: Text(_getRenderResolutionName(context, e)), - subtitle: Text(_getRenderResolutionDescription(context, e)), - selected: currentResolution == e, - onTap: () => changeResolution(e), - ), - ) - .toList(); - }, - ); - } - - void _openAutosaveModal(BuildContext context) { - final cubit = context.read(); - final autosave = cubit.state.autosave; - final showSaveButton = cubit.state.showSaveButton; - final delayed = cubit.state.delayedAutosave; - - showLeapBottomSheet( - context: context, - titleBuilder: (context) => Text(AppLocalizations.of(context).autosave), - childrenBuilder: (context) { - void changeAutosave(bool? autosave, {bool delayed = false}) { - cubit.changeAutosave(autosave, delayed: delayed); - Navigator.of(context).pop(); - } - - return [ - ListTile( - title: Text(AppLocalizations.of(context).yes), - leading: Icon(PhosphorIconsLight.check), - selected: autosave && !showSaveButton && !delayed, - onTap: () => changeAutosave(true), - ), - ListTile( - title: Text(AppLocalizations.of(context).delay), - leading: Icon(PhosphorIconsLight.clock), - selected: autosave && delayed, - onTap: () => changeAutosave(null, delayed: true), - ), - ListTile( - title: Text(AppLocalizations.of(context).yesButShowButtons), - leading: Icon(PhosphorIconsLight.question), - selected: autosave && showSaveButton && !delayed, - onTap: () => changeAutosave(null), - ), - ListTile( - title: Text(AppLocalizations.of(context).no), - leading: Icon(PhosphorIconsLight.x), - selected: !autosave, - onTap: () => changeAutosave(false), - ), - ]; - }, - ); - } -} diff --git a/app/lib/settings/behaviors/persistence.dart b/app/lib/settings/behaviors/persistence.dart deleted file mode 100644 index ec425165869d..000000000000 --- a/app/lib/settings/behaviors/persistence.dart +++ /dev/null @@ -1,311 +0,0 @@ -import 'package:butterfly/api/file_system.dart'; -import 'package:butterfly/repositories/document_state.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:butterfly/src/generated/i18n/app_localizations.dart'; -import 'package:lw_file_system/lw_file_system.dart'; -import 'package:material_leap/material_leap.dart'; -import 'package:phosphor_flutter/phosphor_flutter.dart'; - -import '../../cubits/settings.dart'; - -class PersistenceBehaviorSettings extends StatelessWidget { - const PersistenceBehaviorSettings({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: WindowTitleBar( - title: Text(AppLocalizations.of(context).persistenceDocumentStates), - ), - body: Align( - alignment: Alignment.center, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: LeapBreakpoints.compact), - child: BlocBuilder( - builder: (context, state) { - final settings = state.documentStatePersistence; - void change(DocumentStatePersistenceSettings next) { - context.read().changeDocumentStatePersistence( - next, - ); - } - - return ListView( - children: [ - SwitchListTile( - value: settings.enabled, - secondary: const PhosphorIcon(PhosphorIconsLight.power), - title: Text( - AppLocalizations.of(context).persistentStatesEnabled, - ), - onChanged: (value) => - change(settings.copyWith(enabled: value)), - ), - const Divider(), - SwitchListTile( - value: settings.page, - secondary: const PhosphorIcon(PhosphorIconsLight.file), - title: Text( - AppLocalizations.of(context).persistentStateCurrentPage, - ), - onChanged: settings.enabled - ? (value) => change(settings.copyWith(page: value)) - : null, - ), - SwitchListTile( - value: settings.camera, - secondary: const PhosphorIcon( - PhosphorIconsLight.frameCorners, - ), - title: Text( - AppLocalizations.of(context).persistentStateViewport, - ), - onChanged: settings.enabled - ? (value) => change(settings.copyWith(camera: value)) - : null, - ), - SwitchListTile( - value: settings.locks, - secondary: const PhosphorIcon(PhosphorIconsLight.lockKey), - title: Text(AppLocalizations.of(context).lock), - onChanged: settings.enabled - ? (value) => change(settings.copyWith(locks: value)) - : null, - ), - SwitchListTile( - value: settings.tool, - secondary: const PhosphorIcon(PhosphorIconsLight.toolbox), - title: Text( - AppLocalizations.of(context).persistentStateSelectedTool, - ), - onChanged: settings.enabled - ? (value) => change(settings.copyWith(tool: value)) - : null, - ), - SwitchListTile( - value: settings.navigator, - secondary: const PhosphorIcon(PhosphorIconsLight.sidebar), - title: Text(AppLocalizations.of(context).navigator), - onChanged: settings.enabled - ? (value) => change(settings.copyWith(navigator: value)) - : null, - ), - SwitchListTile( - value: settings.layers, - secondary: const PhosphorIcon(PhosphorIconsLight.stack), - title: Text(AppLocalizations.of(context).layers), - onChanged: settings.enabled - ? (value) => change(settings.copyWith(layers: value)) - : null, - ), - SwitchListTile( - value: settings.areas, - secondary: const PhosphorIcon(PhosphorIconsLight.selection), - title: Text(AppLocalizations.of(context).areas), - onChanged: settings.enabled - ? (value) => change(settings.copyWith(areas: value)) - : null, - ), - const Divider(), - ExactSlider( - header: Text( - AppLocalizations.of(context).persistentStateMaxRecords, - ), - leading: const PhosphorIcon(PhosphorIconsLight.listNumbers), - value: settings.maxEntries.toDouble(), - min: 20, - max: 2000, - defaultValue: 400, - fractionDigits: 0, - onChangeEnd: (value) => - change(settings.copyWith(maxEntries: value.toInt())), - ), - ExactSlider( - header: Text( - AppLocalizations.of( - context, - ).persistentStateDeleteOlderThanDays, - ), - leading: const PhosphorIcon(PhosphorIconsLight.calendar), - value: settings.maxAgeDays.toDouble(), - min: 7, - max: 730, - defaultValue: 180, - fractionDigits: 0, - onChangeEnd: (value) => - change(settings.copyWith(maxAgeDays: value.toInt())), - ), - ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.trash), - title: Text( - AppLocalizations.of(context).persistentStateCleanup, - ), - subtitle: Text( - AppLocalizations.of( - context, - ).persistentStateCleanupDescription, - ), - enabled: settings.enabled, - onTap: settings.enabled - ? () => _cleanupPersistentStates(context) - : null, - ), - ], - ); - }, - ), - ), - ), - ); - } - - Future _cleanupPersistentStates(BuildContext context) async { - final messenger = ScaffoldMessenger.of(context); - final settingsCubit = context.read(); - final fileSystem = context.read(); - final result = await _showCleanupTargetsDialog( - context, - settingsCubit, - fileSystem, - ); - if (result == null) return; - if (!context.mounted) return; - messenger.showSnackBar( - SnackBar( - content: Text( - AppLocalizations.of(context).persistentStateCleanupFeedback( - result.targetCount, - result.deletedRecords, - ), - ), - ), - ); - } - - Future<_CleanupResult?> _showCleanupTargetsDialog( - BuildContext context, - SettingsCubit settingsCubit, - ButterflyFileSystem fileSystem, - ) async { - final settings = settingsCubit.state; - final targets = [ - _CleanupTarget( - id: '', - label: AppLocalizations.of(context).local, - storage: null, - ), - ...settings.connections.map( - (connection) => _CleanupTarget( - id: connection.identifier, - label: connection.identifier, - storage: connection, - ), - ), - ]; - final selected = targets.map((target) => target.id).toSet(); - var cleaning = false; - - return showDialog<_CleanupResult>( - context: context, - builder: (context) => StatefulBuilder( - builder: (context, setState) => AlertDialog( - title: Text(AppLocalizations.of(context).persistentStateCleanup), - scrollable: true, - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (cleaning) ...[ - const LinearProgressIndicator(), - const SizedBox(height: 16), - ], - for (final target in targets) - CheckboxListTile( - value: selected.contains(target.id), - onChanged: cleaning - ? null - : (value) { - setState(() { - if (value ?? false) { - selected.add(target.id); - } else { - selected.remove(target.id); - } - }); - }, - secondary: PhosphorIcon( - target.storage == null - ? PhosphorIconsLight.house - : PhosphorIconsLight.cloud, - ), - title: Text(target.label), - controlAffinity: ListTileControlAffinity.leading, - ), - ], - ), - actions: [ - TextButton( - onPressed: cleaning ? null : () => Navigator.of(context).pop(), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), - ElevatedButton( - onPressed: selected.isEmpty || cleaning - ? null - : () async { - final selectedTargets = targets - .where((target) => selected.contains(target.id)) - .toList(); - setState(() => cleaning = true); - var deleted = 0; - for (final target in selectedTargets) { - final repository = DocumentStateRepository( - fileSystem.buildDocumentStateSystem(target.storage), - settingsProvider: () => - settingsCubit.state.documentStatePersistence, - ); - deleted += await repository.cleanup(); - } - if (!context.mounted) return; - Navigator.of(context).pop( - _CleanupResult( - targetCount: selectedTargets.length, - deletedRecords: deleted, - ), - ); - }, - child: cleaning - ? Text( - AppLocalizations.of( - context, - ).persistentStateCleanupInProgress, - ) - : Text(AppLocalizations.of(context).delete), - ), - ], - ), - ), - ); - } -} - -class _CleanupTarget { - const _CleanupTarget({ - required this.id, - required this.label, - required this.storage, - }); - - final String id; - final String label; - final ExternalStorage? storage; -} - -class _CleanupResult { - const _CleanupResult({ - required this.targetCount, - required this.deletedRecords, - }); - - final int targetCount; - final int deletedRecords; -} diff --git a/app/lib/settings/data.dart b/app/lib/settings/data.dart index 851d05a871f0..13fc919cb8aa 100644 --- a/app/lib/settings/data.dart +++ b/app/lib/settings/data.dart @@ -10,10 +10,8 @@ import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/dialogs/template.dart'; import 'package:butterfly/models/viewport.dart'; -import 'package:butterfly/theme.dart'; import 'package:butterfly/visualizer/connection.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; @@ -24,313 +22,173 @@ import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../dialogs/packs/dialog.dart'; -class DataSettingsPage extends StatefulWidget { - final bool inView; - const DataSettingsPage({super.key, this.inView = false}); - - @override - State createState() => _DataSettingsPageState(); +Widget buildDataDirectorySetting( + BuildContext context, + ButterflySettings state, +) { + return ListTile( + title: Text(AppLocalizations.of(context).dataDirectory), + leading: const PhosphorIcon(PhosphorIconsLight.folder), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + state.documentPath.isNotEmpty + ? FutureBuilder( + future: getButterflyDirectory(), + builder: (context, snapshot) { + if (snapshot.hasData) return Text(snapshot.data!); + return const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator(), + ); + }, + ) + : Text(AppLocalizations.of(context).defaultPath), + if (Platform.isAndroid || Platform.isIOS) + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + AppLocalizations.of(context).platformExperimentalWarning, + style: const TextStyle(color: Colors.red), + ), + ), + ], + ), + onTap: () => changeDataDirectory(context), + trailing: state.documentPath.isNotEmpty + ? IconButton( + icon: const PhosphorIcon(PhosphorIconsLight.clockClockwise), + tooltip: AppLocalizations.of(context).defaultPath, + onPressed: () => + changePath(context, context.read(), ''), + ) + : null, + ); } -class _DataSettingsPageState extends State { - late final DocumentFileSystem _documentSystem; - - @override - void initState() { - super.initState(); - _documentSystem = context.read().buildDocumentSystem(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: widget.inView ? Colors.transparent : null, - appBar: WindowTitleBar( - inView: widget.inView, - backgroundColor: widget.inView ? Colors.transparent : null, - title: Text(AppLocalizations.of(context).data), - ), - body: BlocBuilder( - builder: (context, state) { - return ListView( - children: [ - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (!kIsWeb) ...[ - ListTile( - title: Text(AppLocalizations.of(context).syncMode), - leading: PhosphorIcon(state.syncMode.getIcon()), - subtitle: Text( - state.syncMode.getLocalizedName(context), - ), - onTap: () => _openSyncModeModal(context), - ), - ListTile( - title: Text( - AppLocalizations.of(context).dataDirectory, - ), - leading: const PhosphorIcon( - PhosphorIconsLight.folder, - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - state.documentPath.isNotEmpty - ? FutureBuilder( - future: getButterflyDirectory(), - builder: (context, snapshot) { - if (snapshot.hasData) { - return Text(snapshot.data!); - } - return const SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator(), - ); - }, - ) - : Text( - AppLocalizations.of(context).defaultPath, - ), - if (Platform.isAndroid || Platform.isIOS) - Padding( - padding: const EdgeInsets.only(top: 4.0), - child: Text( - AppLocalizations.of( - context, - ).platformExperimentalWarning, - style: const TextStyle(color: Colors.red), - ), - ), - ], - ), - onTap: _changeDataDirectory, - trailing: state.documentPath.isNotEmpty - ? IconButton( - icon: const PhosphorIcon( - PhosphorIconsLight.clockClockwise, - ), - tooltip: AppLocalizations.of( - context, - ).defaultPath, - onPressed: () => _changePath( - context, - context.read(), - '', - ), - ) - : null, - ), - ], - ListTile( - title: Text(AppLocalizations.of(context).templates), - leading: const PhosphorIcon( - PhosphorIconsLight.file, - textDirection: TextDirection.ltr, - ), - onTap: () => showDialog( - context: context, - builder: (ctx) => const TemplateDialog(), - ), - ), - ListTile( - title: Text(AppLocalizations.of(context).packs), - leading: const PhosphorIcon(PhosphorIconsLight.package), - onTap: () => showDialog( - context: context, - builder: (ctx) => MultiBlocProvider( - providers: [ - BlocProvider( - lazy: false, - create: (ctx) { - final transformCubit = TransformCubit( - MediaQuery.devicePixelRatioOf(context), - ); - return DocumentBloc.placeholder( - context.read(), - EditorController( - context.read(), - transformCubit, - CameraViewport.unbaked(), - ), - context.read(), - ); - }, - ), - ], - child: const PacksDialog(globalOnly: true), - ), - ), - ), - ListTile( - title: Text( - AppLocalizations.of(context).exportAllFiles, - ), - leading: const PhosphorIcon(PhosphorIconsLight.export), - onTap: () => _exportData(context), - ), - ], - ), - ), - ), - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ListTile( - title: Text( - AppLocalizations.of(context).restoreSettingsFromFile, - ), - leading: Icon(PhosphorIconsLight.arrowSquareIn), - onTap: () => _importSettings(context), - ), - ListTile( - title: Text( - AppLocalizations.of(context).exportSettingsToFile, - ), - leading: Icon(PhosphorIconsLight.arrowSquareOut), - onTap: () => _exportSettings(context), - ), - ], - ), - ), - ), - ], - ); - }, +Future changeDataDirectory(BuildContext context) async { + try { + final settingsCubit = context.read(); + final selectedDir = Platform.isAndroid + ? await AndroidSafDirectoryFileSystem.pickDirectory() + : await FilePicker.getDirectoryPath(); + if (selectedDir != null) { + if (!context.mounted) return; + await changePath(context, settingsCubit, selectedDir); + } + } catch (e) { + if (!context.mounted) return; + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(AppLocalizations.of(context).error), + content: Text(e.toString()), ), ); } +} - Future _changeDataDirectory() async { - try { - final settingsCubit = context.read(); - final selectedDir = Platform.isAndroid - ? await AndroidSafDirectoryFileSystem.pickDirectory() - : await FilePicker.getDirectoryPath(); - if (selectedDir != null) { - if (!context.mounted) return; - await _changePath(context, settingsCubit, selectedDir); - } - } catch (e) { - if (!context.mounted) return; - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(AppLocalizations.of(context).error), - content: Text(e.toString()), - ), - ); - } +Future changePath( + BuildContext context, + SettingsCubit settingsCubit, + String newPath, +) async { + var oldPath = settingsCubit.state.documentPath; + final defaultPath = await getButterflyDirectory(usePrefs: false); + if (oldPath.isEmpty) { + oldPath = defaultPath; + } + var movedPath = newPath; + if (movedPath.isEmpty) { + movedPath = defaultPath; } - Future _changePath( - BuildContext context, - SettingsCubit settingsCubit, - String newPath, - ) async { - var oldPath = settingsCubit.state.documentPath; - final defaultPath = await getButterflyDirectory(usePrefs: false); - if (oldPath.isEmpty) { - oldPath = defaultPath; - } - var movedPath = newPath; - if (movedPath.isEmpty) { - movedPath = defaultPath; - } - - final confirm = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(AppLocalizations.of(context).warning), - scrollable: true, - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - AppLocalizations.of(context).changeDataDirectoryWarningContent, - ), - const SizedBox(height: 16), - Card( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - oldPath, - style: TextStyle( - decoration: TextDecoration.lineThrough, - fontFamily: 'monospace', - ), - ), - const SizedBox(height: 4), - Text( - movedPath, - style: const TextStyle(fontFamily: 'monospace'), + final confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(AppLocalizations.of(context).warning), + scrollable: true, + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(AppLocalizations.of(context).changeDataDirectoryWarningContent), + const SizedBox(height: 16), + Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + oldPath, + style: TextStyle( + decoration: TextDecoration.lineThrough, + fontFamily: 'monospace', ), - ], - ), + ), + const SizedBox(height: 4), + Text( + movedPath, + style: const TextStyle(fontFamily: 'monospace'), + ), + ], ), ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), - ElevatedButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text(MaterialLocalizations.of(context).continueButtonLabel), ), ], ), - ); + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), + ), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(MaterialLocalizations.of(context).continueButtonLabel), + ), + ], + ), + ); - if (confirm != true) return; + if (confirm != true) return; - if (!(await _documentSystem.moveAbsolute(oldPath, movedPath)) && - newPath.isNotEmpty) { - return; - } - settingsCubit.changeDocumentPath(newPath); + final documentSystem = context + .read() + .buildDocumentSystem(); + if (!(await documentSystem.moveAbsolute(oldPath, movedPath)) && + newPath.isNotEmpty) { + return; } + settingsCubit.changeDocumentPath(newPath); +} - Future _openSyncModeModal(BuildContext context) => showLeapBottomSheet( - context: context, - titleBuilder: (ctx) => Text(AppLocalizations.of(ctx).syncMode), - childrenBuilder: (ctx) { - final settingsCubit = context.read(); - void changeSyncMode(SyncMode syncMode) { - settingsCubit.changeSyncMode(syncMode); - Navigator.of(context).pop(); - } +Future openSyncModeModal(BuildContext context) => showLeapBottomSheet( + context: context, + titleBuilder: (ctx) => Text(AppLocalizations.of(ctx).syncMode), + childrenBuilder: (ctx) { + final settingsCubit = context.read(); + void changeSyncMode(SyncMode syncMode) { + settingsCubit.changeSyncMode(syncMode); + Navigator.of(context).pop(); + } - return [ - ...SyncMode.values.map((syncMode) { - return ListTile( - title: Text(syncMode.getLocalizedName(context)), - leading: PhosphorIcon(syncMode.getIcon()), - selected: syncMode == settingsCubit.state.syncMode, - onTap: () => changeSyncMode(syncMode), - ); - }), - const SizedBox(height: 32), - ]; - }, - ); + return [ + ...SyncMode.values.map((syncMode) { + return ListTile( + title: Text(syncMode.getLocalizedName(context)), + leading: PhosphorIcon(syncMode.getIcon()), + selected: syncMode == settingsCubit.state.syncMode, + onTap: () => changeSyncMode(syncMode), + ); + }), + const SizedBox(height: 32), + ]; + }, +); - /* +/* Future _openIceServersModal(BuildContext context) { final settingsCubit = context.read(); return showLeapBottomSheet( @@ -386,239 +244,273 @@ class _DataSettingsPageState extends State { }); } */ - Future _exportData(BuildContext context) async { - final localizations = AppLocalizations.of(context); - bool exportDocuments = true; - bool exportPacks = true; - bool exportTemplates = true; - double? exportProgress; +void openTemplatesDialog(BuildContext context) { + showDialog(context: context, builder: (ctx) => const TemplateDialog()); +} - await showDialog( - context: context, - barrierDismissible: false, - builder: (context) { - return StatefulBuilder( - builder: (context, setState) { - final isExporting = exportProgress != null; - return AlertDialog( - title: Text(localizations.exportAllFiles), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CheckboxListTile( - title: Text(localizations.files), - value: exportDocuments, - onChanged: isExporting - ? null - : (value) => - setState(() => exportDocuments = value ?? false), - ), - CheckboxListTile( - title: Text(localizations.packs), - value: exportPacks, - onChanged: isExporting - ? null - : (value) => - setState(() => exportPacks = value ?? false), - ), - CheckboxListTile( - title: Text(localizations.templates), - value: exportTemplates, - onChanged: isExporting - ? null - : (value) => - setState(() => exportTemplates = value ?? false), - ), - ], +void openPacksDialog(BuildContext context) { + showDialog( + context: context, + builder: (ctx) => MultiBlocProvider( + providers: [ + BlocProvider( + lazy: false, + create: (ctx) { + final transformCubit = TransformCubit( + MediaQuery.devicePixelRatioOf(context), + ); + return DocumentBloc.placeholder( + context.read(), + EditorController( + context.read(), + transformCubit, + CameraViewport.unbaked(), ), - actions: [ - TextButton( - onPressed: isExporting + context.read(), + ); + }, + ), + ], + child: const PacksDialog(globalOnly: true), + ), + ); +} + +Future exportData(BuildContext context) async { + final localizations = AppLocalizations.of(context); + bool exportDocuments = true; + bool exportPacks = true; + bool exportTemplates = true; + double? exportProgress; + + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return StatefulBuilder( + builder: (context, setState) { + final isExporting = exportProgress != null; + return AlertDialog( + title: Text(localizations.exportAllFiles), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CheckboxListTile( + title: Text(localizations.files), + value: exportDocuments, + onChanged: isExporting ? null - : () => Navigator.of(context).pop(), - child: Text( - MaterialLocalizations.of(context).cancelButtonLabel, - ), + : (value) => + setState(() => exportDocuments = value ?? false), + ), + CheckboxListTile( + title: Text(localizations.packs), + value: exportPacks, + onChanged: isExporting + ? null + : (value) => setState(() => exportPacks = value ?? false), ), - TextButton( - onPressed: isExporting + CheckboxListTile( + title: Text(localizations.templates), + value: exportTemplates, + onChanged: isExporting ? null - : () async { - if (!exportDocuments && - !exportPacks && - !exportTemplates) { - return; + : (value) => + setState(() => exportTemplates = value ?? false), + ), + ], + ), + actions: [ + TextButton( + onPressed: isExporting + ? null + : () => Navigator.of(context).pop(), + child: Text( + MaterialLocalizations.of(context).cancelButtonLabel, + ), + ), + TextButton( + onPressed: isExporting + ? null + : () async { + if (!exportDocuments && + !exportPacks && + !exportTemplates) { + return; + } + + setState(() => exportProgress = -1.0); + + try { + final fs = context.read(); + final archive = Archive(); + final multiple = + [ + exportDocuments, + exportPacks, + exportTemplates, + ].where((e) => e).length > + 1; + + List packKeys = []; + List templateKeys = []; + + if (exportPacks) { + packKeys = await fs.buildPackSystem().getKeys(); + } + if (exportTemplates) { + templateKeys = await fs + .buildTemplateSystem() + .getKeys(); } - setState(() => exportProgress = -1.0); - - try { - final fs = context.read(); - final archive = Archive(); - final multiple = - [ - exportDocuments, - exportPacks, - exportTemplates, - ].where((e) => e).length > - 1; - - List packKeys = []; - List templateKeys = []; - - if (exportPacks) { - packKeys = await fs.buildPackSystem().getKeys(); - } - if (exportTemplates) { - templateKeys = await fs - .buildTemplateSystem() - .getKeys(); + int totalTasks = + (exportDocuments ? 1 : 0) + + packKeys.length + + templateKeys.length + + 1; // +1 for zip + int completedTasks = 0; + + void updateProgress() { + if (totalTasks > 0) { + setState( + () => exportProgress = + completedTasks / totalTasks, + ); } + } - int totalTasks = - (exportDocuments ? 1 : 0) + - packKeys.length + - templateKeys.length + - 1; // +1 for zip - int completedTasks = 0; - - void updateProgress() { - if (totalTasks > 0) { - setState( - () => exportProgress = - completedTasks / totalTasks, - ); - } + updateProgress(); + + if (exportDocuments) { + final documentSystem = context + .read() + .buildDocumentSystem(); + final directory = await documentSystem.fileSystem + .getRootDirectory(listLevel: allListLevel); + final docArchive = exportDirectory(directory); + for (final file in docArchive.files) { + archive.addFile( + ArchiveFile.bytes( + multiple + ? 'Documents/${file.name}' + : file.name, + file.content as List, + ), + ); } - + completedTasks++; updateProgress(); + } - if (exportDocuments) { - final directory = await _documentSystem.fileSystem - .getRootDirectory(listLevel: allListLevel); - final docArchive = exportDirectory(directory); - for (final file in docArchive.files) { + if (exportPacks) { + final packSystem = fs.buildPackSystem(); + for (final key in packKeys) { + final data = await packSystem.fileSystem.getFile( + key, + ); + if (data != null) { archive.addFile( ArchiveFile.bytes( - multiple - ? 'Documents/${file.name}' - : file.name, - file.content as List, + multiple ? 'Packs/$key' : key, + data, ), ); } completedTasks++; updateProgress(); } + } - if (exportPacks) { - final packSystem = fs.buildPackSystem(); - for (final key in packKeys) { - final data = await packSystem.fileSystem - .getFile(key); - if (data != null) { - archive.addFile( - ArchiveFile.bytes( - multiple ? 'Packs/$key' : key, - data, - ), - ); - } - completedTasks++; - updateProgress(); - } - } - - if (exportTemplates) { - final templateSystem = fs.buildTemplateSystem(); - for (final key in templateKeys) { - final data = await templateSystem.fileSystem - .getFile(key); - if (data != null) { - archive.addFile( - ArchiveFile.bytes( - multiple ? 'Templates/$key' : key, - data, - ), - ); - } - completedTasks++; - updateProgress(); + if (exportTemplates) { + final templateSystem = fs.buildTemplateSystem(); + for (final key in templateKeys) { + final data = await templateSystem.fileSystem + .getFile(key); + if (data != null) { + archive.addFile( + ArchiveFile.bytes( + multiple ? 'Templates/$key' : key, + data, + ), + ); } + completedTasks++; + updateProgress(); } + } - // Small delay to allow UI to render the 99% progress - // before ZipEncoder blocks the thread synchronously. - await Future.delayed( - const Duration(milliseconds: 50), - ); + // Small delay to allow UI to render the 99% progress + // before ZipEncoder blocks the thread synchronously. + await Future.delayed( + const Duration(milliseconds: 50), + ); - final encoder = ZipEncoder(); - final bytes = encoder.encodeBytes(archive); - completedTasks++; - updateProgress(); + final encoder = ZipEncoder(); + final bytes = encoder.encodeBytes(archive); + completedTasks++; + updateProgress(); - if (context.mounted) { - Navigator.of(context).pop(); - exportZip(context, bytes); - } - } catch (e) { - if (context.mounted) { - setState(() => exportProgress = null); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.toString())), - ); - } + if (context.mounted) { + Navigator.of(context).pop(); + exportZip(context, bytes); } - }, - child: isExporting - ? SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - value: exportProgress! < 0 ? null : exportProgress, - ), - ) - : Text(localizations.export), - ), - ], - ); - }, - ); - }, - ); - } + } catch (e) { + if (context.mounted) { + setState(() => exportProgress = null); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.toString())), + ); + } + } + }, + child: isExporting + ? SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + value: exportProgress! < 0 ? null : exportProgress, + ), + ) + : Text(localizations.export), + ), + ], + ); + }, + ); + }, + ); +} - void _importSettings(BuildContext context) async { - final settingsCubit = context.read(); - final result = await FilePicker.pickFile( - type: FileType.custom, - allowedExtensions: ['json'], - ); - final bytes = await result?.readAsBytes(); - if (bytes == null) return; - final data = utf8.decode(bytes); - try { - await settingsCubit.importSettings(data); - } catch (e) { - if (!context.mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(e.toString()))); - } +void importSettings(BuildContext context) async { + final settingsCubit = context.read(); + final result = await FilePicker.pickFile( + type: FileType.custom, + allowedExtensions: ['json'], + ); + final bytes = await result?.readAsBytes(); + if (bytes == null) return; + final data = utf8.decode(bytes); + try { + await settingsCubit.importSettings(data); + } catch (e) { + if (!context.mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(e.toString()))); } +} - void _exportSettings(BuildContext context) async { - final settingsCubit = context.read(); - await exportFile( - bytes: utf8.encode(await settingsCubit.exportSettings()), - context: context, - fileExtension: 'json', - fileName: 'settings', - label: AppLocalizations.of(context).exportSettingsToFile, - mimeType: 'application/json', - uniformTypeIdentifier: 'public.json', - ); - } +void exportSettings(BuildContext context) async { + final settingsCubit = context.read(); + await exportFile( + bytes: utf8.encode(await settingsCubit.exportSettings()), + context: context, + fileExtension: 'json', + fileName: 'settings', + label: AppLocalizations.of(context).exportSettingsToFile, + mimeType: 'application/json', + uniformTypeIdentifier: 'public.json', + ); } diff --git a/app/lib/settings/experiments.dart b/app/lib/settings/experiments.dart deleted file mode 100644 index e282f04d90b8..000000000000 --- a/app/lib/settings/experiments.dart +++ /dev/null @@ -1,127 +0,0 @@ -import 'package:butterfly/api/open.dart'; -import 'package:butterfly/cubits/settings.dart'; -import 'package:butterfly/theme.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:butterfly/src/generated/i18n/app_localizations.dart'; -import 'package:material_leap/material_leap.dart'; -import 'package:phosphor_flutter/phosphor_flutter.dart'; - -class ExperimentsSettingsPage extends StatelessWidget { - final bool inView; - const ExperimentsSettingsPage({super.key, this.inView = false}); - - List<({String name, String description, IconGetter icon})> _getExperiments( - BuildContext context, - ) => [ - ( - name: 'collaboration', - description: AppLocalizations.of(context).collaboration, - icon: PhosphorIcons.chatTeardrop, - ), - ( - name: 'smoothNavigation', - description: AppLocalizations.of(context).smoothNavigation, - icon: PhosphorIcons.caretCircleDoubleDown, - ), - ( - name: 'edgePanAreaSwitching', - description: AppLocalizations.of(context).edgePanAreaSwitching, - icon: PhosphorIcons.cursor, - ), - ]; - - static const Map _featureHelps = { - 'collaboration': 'collaboration', - }; - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: inView ? Colors.transparent : null, - appBar: WindowTitleBar( - title: Text(AppLocalizations.of(context).experiments), - backgroundColor: inView ? Colors.transparent : null, - inView: inView, - actions: [ - IconButton( - icon: const PhosphorIcon(PhosphorIconsLight.clockCounterClockwise), - tooltip: LeapLocalizations.of(context).reset, - onPressed: () => context.read().resetFlags(), - ), - ], - ), - body: BlocBuilder( - buildWhen: (previous, current) => previous.flags != current.flags, - builder: (context, state) { - final experiments = _getExperiments(context); - if (experiments.isEmpty) { - return Center(child: Text(AppLocalizations.of(context).noElements)); - } - return ListView( - children: [ - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - children: [ - Center( - child: Padding( - padding: const EdgeInsets.all(8), - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 16, - children: [ - Icon(PhosphorIconsLight.warning), - Flexible( - child: Text( - AppLocalizations.of( - context, - ).experimentsWarning, - ), - ), - ], - ), - ), - ), - SizedBox(height: 16), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: experiments.length, - itemBuilder: (context, index) { - final experiment = experiments[index]; - final currentHelp = _featureHelps[experiment.name]; - final enabled = state.hasFlag(experiment.name); - return AdvancedSwitchListTile( - value: enabled, - onChanged: (value) { - final cubit = context.read(); - if (value == true) { - cubit.addFlag(experiment.name); - } else { - cubit.removeFlag(experiment.name); - } - }, - title: Text(experiment.description), - leading: PhosphorIcon( - experiment.icon(PhosphorIconsStyle.light), - ), - onTap: currentHelp == null - ? null - : () => openHelp([currentHelp]), - ); - }, - ), - ], - ), - ), - ), - ], - ); - }, - ), - ); - } -} diff --git a/app/lib/settings/general.dart b/app/lib/settings/general.dart deleted file mode 100644 index f8b79099349f..000000000000 --- a/app/lib/settings/general.dart +++ /dev/null @@ -1,331 +0,0 @@ -import 'dart:convert'; - -import 'package:butterfly/api/open.dart'; -import 'package:butterfly/cubits/settings.dart'; -import 'package:butterfly/main.dart'; -import 'package:butterfly/theme.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:material_leap/material_leap.dart'; -import 'package:phosphor_flutter/phosphor_flutter.dart'; -import 'package:butterfly/src/generated/i18n/app_localizations.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:http/http.dart' as http; - -@immutable -class Meta { - final String stableVersion, nightlyVersion, developVersion, mainVersion; - - const Meta({ - required this.stableVersion, - required this.nightlyVersion, - required this.developVersion, - required this.mainVersion, - }); - Meta.fromJson(Map json) - : stableVersion = json['version']?['stable'] ?? '?', - nightlyVersion = json['version']?['nightly'] ?? '?', - developVersion = json['version']?['develop'] ?? '?', - mainVersion = json['version']?['main'] ?? '?'; -} - -class GeneralSettingsPage extends StatefulWidget { - final bool inView; - const GeneralSettingsPage({super.key, this.inView = false}); - - @override - State createState() => _GeneralSettingsPageState(); -} - -class _GeneralSettingsPageState extends State { - Future? _metaFuture; - final Future _currentVersion = getCurrentVersion(); - - void loadMeta() => setState(() { - _metaFuture = _fetchMeta(); - }); - - Future _fetchMeta() async { - final response = await http.get( - Uri.parse('https://butterfly.linwood.dev/meta.json'), - ); - return Meta.fromJson({...json.decode(response.body)}); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: widget.inView ? Colors.transparent : null, - appBar: WindowTitleBar( - title: Text(AppLocalizations.of(context).general), - backgroundColor: widget.inView ? Colors.transparent : null, - inView: widget.inView, - ), - body: FutureBuilder( - future: _currentVersion, - builder: (context, snapshot) { - final currentVersion = snapshot.data ?? '?'; - final currentVersionName = '$applicationVersionName $currentVersion'; - return ListView( - children: [ - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: settingsCardTitlePadding, - child: Text( - AppLocalizations.of(context).update, - style: TextTheme.of(context).headlineSmall, - ), - ), - const SizedBox(height: 16), - ListTile( - title: Text( - AppLocalizations.of(context).currentVersion, - ), - subtitle: Text(currentVersionName), - onTap: () => saveToClipboard(context, currentVersion), - ), - if (!kIsWeb) - FutureBuilder( - future: _metaFuture, - builder: (context, snapshot) { - if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); - } - if (snapshot.connectionState == - ConnectionState.waiting) { - return const Center( - child: CircularProgressIndicator(), - ); - } - if (!snapshot.hasData) { - return ListTile( - title: Text( - AppLocalizations.of(context).checkForUpdates, - ), - subtitle: Text( - AppLocalizations.of( - context, - ).checkForUpdatesWarning, - ), - onTap: loadMeta, - ); - } - final meta = snapshot.data!; - final stableVersion = meta.stableVersion; - final nightlyVersion = meta.nightlyVersion; - final developVersion = meta.developVersion; - final mainVersion = meta.mainVersion; - final isStable = currentVersion == stableVersion; - final isNightly = currentVersion == nightlyVersion; - final isDevelop = currentVersion == developVersion; - final isMain = currentVersion == mainVersion; - final isError = - meta.nightlyVersion == '?' || - meta.stableVersion == '?'; - final isUpdateAvailable = - !isError && - !isStable && - !isNightly && - !isDevelop && - !isMain; - return Column( - children: [ - ListTile( - title: Text( - AppLocalizations.of(context).stable, - ), - subtitle: Text(stableVersion), - onTap: () => - saveToClipboard(context, stableVersion), - ), - ListTile( - title: Text( - AppLocalizations.of(context).nightly, - ), - subtitle: Text(nightlyVersion), - onTap: () => - saveToClipboard(context, nightlyVersion), - ), - const Divider(), - if (isStable) ...[ - ListTile( - title: Text( - AppLocalizations.of( - context, - ).usingLatestStable, - ), - ), - ] else if (isNightly || - isDevelop || - isMain) ...[ - ListTile( - title: Text( - AppLocalizations.of( - context, - ).usingLatestNightly, - ), - ), - ] else if (isError) ...[ - ListTile( - title: Text( - AppLocalizations.of(context).error, - ), - ), - ] else if (isUpdateAvailable) - ListTile( - title: Text( - AppLocalizations.of( - context, - ).updateAvailable, - ), - subtitle: Text( - AppLocalizations.of(context).updateNow, - ), - leading: const PhosphorIcon( - PhosphorIconsLight.arrowRight, - ), - onTap: () async { - await launchUrl( - Uri.parse( - 'https://butterfly.linwood.dev/downloads', - ), - mode: LaunchMode.externalApplication, - ); - }, - ), - ], - ); - }, - ), - ], - ), - ), - ), - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.article), - title: Text(AppLocalizations.of(context).documentation), - onTap: () => launchUrl( - Uri.https('butterfly.linwood.dev', ''), - mode: LaunchMode.externalApplication, - ), - ), - ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.flag), - title: Text(AppLocalizations.of(context).releaseNotes), - onTap: () => openReleaseNotes(), - ), - ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.users), - title: const Text('Matrix'), - onTap: () => launchUrl( - Uri.https('go.linwood.dev', 'matrix'), - mode: LaunchMode.externalApplication, - ), - ), - ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.users), - title: const Text('Discord'), - onTap: () => launchUrl( - Uri.https('go.linwood.dev', 'discord'), - mode: LaunchMode.externalApplication, - ), - ), - ListTile( - leading: const PhosphorIcon( - PhosphorIconsLight.translate, - ), - title: Text(AppLocalizations.of(context).translate), - onTap: () => launchUrl( - Uri.https('go.linwood.dev', 'butterfly/translate'), - mode: LaunchMode.externalApplication, - ), - ), - ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.code), - title: Text(AppLocalizations.of(context).sourceCode), - onTap: () => launchUrl( - Uri.https('go.linwood.dev', 'butterfly/source'), - mode: LaunchMode.externalApplication, - ), - ), - ListTile( - leading: const PhosphorIcon( - PhosphorIconsLight.arrowCounterClockwise, - ), - title: Text(AppLocalizations.of(context).changelog), - onTap: () => launchUrl( - Uri.https('butterfly.linwood.dev', 'changelog'), - mode: LaunchMode.externalApplication, - ), - ), - ], - ), - ), - ), - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.stack), - title: Text(AppLocalizations.of(context).license), - onTap: () => launchUrl( - Uri.https('go.linwood.dev', 'butterfly/license'), - mode: LaunchMode.externalApplication, - ), - ), - ListTile( - leading: const PhosphorIcon( - PhosphorIconsLight.identificationCard, - ), - title: Text(AppLocalizations.of(context).imprint), - onTap: () => launchUrl( - Uri.https('go.linwood.dev', 'imprint'), - mode: LaunchMode.externalApplication, - ), - ), - ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.shield), - title: Text(AppLocalizations.of(context).privacypolicy), - onTap: () => launchUrl( - Uri.https('butterfly.linwood.dev', 'privacypolicy'), - mode: LaunchMode.externalApplication, - ), - ), - ListTile( - title: Text( - AppLocalizations.of(context).thirdPartyLicenses, - ), - leading: const PhosphorIcon( - PhosphorIconsLight.file, - textDirection: TextDirection.ltr, - ), - onTap: () => showLicensePage(context: context), - ), - ], - ), - ), - ), - ], - ); - }, - ), - ); - } -} diff --git a/app/lib/settings/home.dart b/app/lib/settings/home.dart index d395514078e6..ab83660ddd0c 100644 --- a/app/lib/settings/home.dart +++ b/app/lib/settings/home.dart @@ -1,180 +1,150 @@ -import 'package:butterfly/settings/behaviors/home.dart'; -import 'package:butterfly/settings/inputs/home.dart'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:butterfly/api/file_system.dart'; +import 'package:butterfly/api/open.dart'; +import 'package:butterfly/cubits/settings.dart'; +import 'package:butterfly/main.dart'; +import 'package:butterfly/repositories/document_state.dart'; +import 'package:butterfly/services/logger.dart'; import 'package:butterfly/settings/data.dart'; -import 'package:butterfly/settings/personalization.dart'; -import 'package:butterfly/settings/view.dart'; +import 'package:butterfly/settings/inputs/keyboard.dart'; +import 'package:butterfly/settings/inputs/mouse.dart'; +import 'package:butterfly/settings/inputs/pen.dart'; +import 'package:butterfly/settings/inputs/touch.dart'; +import 'package:butterfly/theme.dart'; +import 'package:butterfly/visualizer/connection.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; +import 'package:flutter_localized_locales/flutter_localized_locales.dart'; import 'package:go_router/go_router.dart'; +import 'package:html/parser.dart' as html_parser; +import 'package:image/image.dart' as img; +import 'package:intl/intl.dart' show DateFormat; +import 'package:lw_file_system/lw_file_system.dart'; import 'package:material_leap/material_leap.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; +import 'package:settings_leap/settings_leap.dart'; +import 'package:talker_flutter/talker_flutter.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:http/http.dart' as http; -import 'experiments.dart'; -import 'general.dart'; -import 'connections.dart'; -import 'logs.dart'; - -enum SettingsView { - general, - data, - behaviors, - inputs, - personalization, - view, - connections, - experiments, - logs; +part 'pages/behaviors/home.dart'; +part 'pages/behaviors/persistence.dart'; +part 'pages/connections.dart'; +part 'pages/data.dart'; +part 'pages/experiments.dart'; +part 'pages/general.dart'; +part 'pages/inputs.dart'; +part 'pages/logs.dart'; +part 'pages/personalization.dart'; +part 'pages/view.dart'; - bool get isEnabled => this != SettingsView.connections || !kIsWeb; +final settingsTree = SettingsLeapTree({ + 'general': _generalSettingsPage, + 'data': _dataSettingsPage, + 'behaviors': _behaviorsSettingsPage, + 'inputs': _inputsSettingsPage, + 'personalization': _personalizationSettingsPage, + 'view': _viewSettingsPage, + 'connections': _connectionsSettingsPage, + 'experiments': _experimentsSettingsPage, + 'logs': _logsSettingsPage, +}); - String getLocalizedName(BuildContext context) => switch (this) { - SettingsView.general => AppLocalizations.of(context).general, - SettingsView.data => AppLocalizations.of(context).data, - SettingsView.behaviors => AppLocalizations.of(context).behaviors, - SettingsView.inputs => AppLocalizations.of(context).inputs, - SettingsView.personalization => AppLocalizations.of( - context, - ).personalization, - SettingsView.view => AppLocalizations.of(context).view, - SettingsView.connections => AppLocalizations.of(context).connections, - SettingsView.experiments => AppLocalizations.of(context).experiments, - SettingsView.logs => AppLocalizations.of(context).logs, - }; +class SettingsPage extends StatelessWidget { + final bool inView; + const SettingsPage({super.key, this.inView = false}); - IconGetter get icon => switch (this) { - SettingsView.general => PhosphorIcons.gear, - SettingsView.data => PhosphorIcons.database, - SettingsView.behaviors => PhosphorIcons.faders, - SettingsView.inputs => PhosphorIcons.keyboard, - SettingsView.personalization => PhosphorIcons.monitor, - SettingsView.view => PhosphorIcons.eye, - SettingsView.connections => PhosphorIcons.cloud, - SettingsView.experiments => PhosphorIcons.flask, - SettingsView.logs => PhosphorIcons.bug, - }; - String get path => '/settings/$name'; -} + @override + Widget build(BuildContext context) { + final child = BlocBuilder( + builder: (context, state) => SettingsLeapView( + tree: settingsTree, + state: state, + title: (context) => AppLocalizations.of(context).settings, + searchHint: (context) => AppLocalizations.of(context).search, + isDialog: inView, + compactWidth: LeapBreakpoints.compact, + closeButton: IconButton.outlined( + icon: const PhosphorIcon(PhosphorIconsLight.x), + onPressed: () => Navigator.of(context).maybePop(), + tooltip: MaterialLocalizations.of(context).closeButtonTooltip, + ), + ), + ); -class SettingsPage extends StatefulWidget { - final bool isDialog; - const SettingsPage({super.key, this.isDialog = false}); + if (inView) return child; - @override - State createState() => _SettingsPageState(); + return Scaffold( + appBar: WindowTitleBar( + title: Text(AppLocalizations.of(context).settings), + inView: inView, + ), + body: child, + ); + } } -class _SettingsPageState extends State { - SettingsView _view = SettingsView.general; +class SettingsDetailsPage extends StatelessWidget { + final String id; + final bool inView; + + const SettingsDetailsPage({super.key, required this.id, this.inView = false}); @override Widget build(BuildContext context) { - final size = MediaQuery.sizeOf(context); - final isMobile = size.width < LeapBreakpoints.compact; - final content = switch (_view) { - SettingsView.general => const GeneralSettingsPage(inView: true), - SettingsView.data => const DataSettingsPage(inView: true), - SettingsView.behaviors => const BehaviorsSettingsPage(inView: true), - SettingsView.inputs => const InputsSettingsPage(inView: true), - SettingsView.personalization => const PersonalizationSettingsPage( - inView: true, - ), - SettingsView.view => const ViewSettingsPage(inView: true), - SettingsView.connections => const ConnectionsSettingsPage(inView: true), - SettingsView.experiments => const ExperimentsSettingsPage(inView: true), - SettingsView.logs => const LogsSettingsPage(inView: true), - }; + // Search whole tree for page with id + SettingsLeapPage? findPage( + Map> tree, + String id, + ) { + for (final entry in tree.entries) { + if (entry.key == id) return entry.value; + final page = findPage(entry.value.children, id); + if (page != null) return page; + } + return null; + } - final drawer = NavigationDrawer( - selectedIndex: SettingsView.values - .where((e) => e.isEnabled) - .toList() - .indexOf(_view), - onDestinationSelected: (index) { - final view = SettingsView.values - .where((e) => e.isEnabled) - .toList()[index]; - setState(() { - _view = view; - }); - if (isMobile) { - Navigator.of(context).pop(); - } - }, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(28, 16, 16, 16), - child: Row( - spacing: 16, - children: [ - if (widget.isDialog && !isMobile) - IconButton.outlined( - icon: const PhosphorIcon(PhosphorIconsLight.x), - onPressed: () => Navigator.of(context).pop(), - tooltip: MaterialLocalizations.of(context).closeButtonTooltip, - ), - Expanded( - child: Text( - AppLocalizations.of(context).settings, - style: TextTheme.of(context).headlineSmall, - ), - ), - ], - ), + final page = findPage(settingsTree.pages, id); + if (page == null) { + return Scaffold( + appBar: WindowTitleBar( + title: Text(AppLocalizations.of(context).settings), + inView: inView, ), - ...SettingsView.values.where((e) => e.isEnabled).map((view) { - return NavigationDrawerDestination( - icon: PhosphorIcon(view.icon(PhosphorIconsStyle.light)), - selectedIcon: PhosphorIcon(view.icon(PhosphorIconsStyle.fill)), - label: Text(view.getLocalizedName(context)), - ); - }), - ], - ); - - final child = isMobile - ? Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.isDialog) - Header( - leading: IconButton.outlined( - icon: const PhosphorIcon(PhosphorIconsLight.x), - onPressed: () => Navigator.of(context).pop(), - tooltip: MaterialLocalizations.of( - context, - ).closeButtonTooltip, - ), - title: Text(AppLocalizations.of(context).settings), - ), - ListView( - shrinkWrap: true, - children: SettingsView.values.where((e) => e.isEnabled).map(( - view, - ) { - return ListTile( - leading: PhosphorIcon(view.icon(PhosphorIconsStyle.light)), - title: Text(view.getLocalizedName(context)), - onTap: () => context.push(view.path), - ); - }).toList(), - ), - const SizedBox(height: 16), - ], - ) - : Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - drawer, - Expanded(child: content), - ], - ); - - if (widget.isDialog) return child; - - return Scaffold( - appBar: AppBar(title: Text(AppLocalizations.of(context).settings)), - body: child, + body: Center(child: Text(AppLocalizations.of(context).error)), + ); + } + return BlocBuilder( + builder: (context, state) => SettingsLeapGeneratedPage( + page: page, + state: state, + inView: inView, + cardMargin: settingsCardMargin, + cardPadding: settingsCardPadding, + sectionTitlePadding: settingsCardTitlePadding, + ), ); } } + +PreferredSizeWidget _butterflyAppBar( + BuildContext context, + ButterflySettings state, + bool inView, + Widget title, + List? actions, +) => WindowTitleBar( + title: title, + backgroundColor: inView ? Colors.transparent : null, + inView: inView, + actions: actions ?? const [], +); diff --git a/app/lib/settings/inputs/home.dart b/app/lib/settings/inputs/home.dart deleted file mode 100644 index a9d39ded4d66..000000000000 --- a/app/lib/settings/inputs/home.dart +++ /dev/null @@ -1,232 +0,0 @@ -import 'dart:ui'; - -import 'package:butterfly/cubits/settings.dart'; -import 'package:butterfly/theme.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:butterfly/src/generated/i18n/app_localizations.dart'; -import 'package:go_router/go_router.dart'; -import 'package:material_leap/material_leap.dart'; -import 'package:phosphor_flutter/phosphor_flutter.dart'; - -class InputsSettingsPage extends StatelessWidget { - final bool inView; - - const InputsSettingsPage({super.key, this.inView = false}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: inView ? Colors.transparent : null, - appBar: WindowTitleBar( - title: Text(AppLocalizations.of(context).inputs), - backgroundColor: inView ? Colors.transparent : null, - inView: inView, - ), - body: BlocBuilder( - builder: (context, state) { - return ListView( - children: [ - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.mouse), - title: Text(AppLocalizations.of(context).mouse), - onTap: () => context.push('/settings/inputs/mouse'), - ), - ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.hand), - title: Text(AppLocalizations.of(context).touch), - onTap: () => context.push('/settings/inputs/touch'), - ), - ListTile( - leading: const PhosphorIcon( - PhosphorIconsLight.keyboard, - ), - title: Text(AppLocalizations.of(context).keyboard), - onTap: () => context.push('/settings/inputs/keyboard'), - ), - ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.pen), - title: Text(AppLocalizations.of(context).pen), - onTap: () => context.push('/settings/inputs/pen'), - ), - ], - ), - ), - ), - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: settingsCardTitlePadding, - child: Column( - spacing: 8, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - AppLocalizations.of(context).sensitivity, - style: TextTheme.of(context).headlineSmall, - ), - Text(AppLocalizations.of(context).sensitivityHint), - ], - ), - ), - const SizedBox(height: 16), - ExactSlider( - min: 10, - max: 1000, - defaultValue: 100, - fractionDigits: 0, - value: state.selectSensitivity * 100, - header: Text(AppLocalizations.of(context).select), - onChangeEnd: (value) { - final cubit = context.read(); - cubit.changeSelectSensitivity(value / 100); - }, - ), - ExactSlider( - min: 10, - max: 1000, - defaultValue: 100, - fractionDigits: 0, - value: state.touchSensitivity * 100, - header: Text(AppLocalizations.of(context).touch), - onChangeEnd: (value) { - final cubit = context.read(); - cubit.changeTouchSensitivity(value / 100); - }, - ), - ExactSlider( - min: 10, - max: 1000, - defaultValue: 100, - value: state.gestureSensitivity * 100, - fractionDigits: 0, - header: Text( - AppLocalizations.of(context).inputGestures, - ), - onChangeEnd: (value) { - final cubit = context.read(); - cubit.changeGestureSensitivity(value / 100); - }, - ), - ExactSlider( - min: 10, - max: 1000, - defaultValue: 100, - value: state.scrollSensitivity * 100, - header: Text(AppLocalizations.of(context).scroll), - fractionDigits: 0, - onChangeEnd: (value) { - final cubit = context.read(); - cubit.changeScrollSensitivity(value / 100); - }, - ), - ], - ), - ), - ), - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: _PointerTest(), - ), - ), - ], - ); - }, - ), - ); - } -} - -class _PointerTest extends StatefulWidget { - const _PointerTest(); - - @override - State<_PointerTest> createState() => __PointerTestState(); -} - -class __PointerTestState extends State<_PointerTest> { - PointerDeviceKind? _kind; - int _buttons = 0; - double? _pressure, _pressureMin, _pressureMax; - Color? _pressed; - - Null Function(PointerEvent event) _changeInputTest(Color? color) => - (PointerEvent event) { - setState(() { - _kind = event.kind; - _buttons = event.buttons; - _pressure = event.pressure; - _pressureMin = event.pressureMin; - _pressureMax = event.pressureMax; - _pressed = color; - }); - }; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: settingsCardTitlePadding, - child: Text( - AppLocalizations.of(context).pointerTest, - style: TextTheme.of(context).headlineSmall, - ), - ), - const SizedBox(height: 16), - SizedBox( - height: 150, - child: Listener( - onPointerMove: _changeInputTest(Colors.blue), - onPointerDown: _changeInputTest(Colors.green), - onPointerUp: _changeInputTest(null), - onPointerCancel: _changeInputTest(Colors.red), - onPointerPanZoomStart: _changeInputTest(Colors.purple), - onPointerPanZoomUpdate: _changeInputTest(Colors.purple[700]), - onPointerPanZoomEnd: _changeInputTest(Colors.purple[900]), - child: Material(color: _pressed), - ), - ), - const SizedBox(height: 8), - ListTile( - title: Text(AppLocalizations.of(context).type), - subtitle: Text(switch (_kind) { - PointerDeviceKind.touch => AppLocalizations.of(context).touch, - PointerDeviceKind.mouse => AppLocalizations.of(context).mouse, - PointerDeviceKind.stylus => AppLocalizations.of(context).pen, - PointerDeviceKind.invertedStylus => AppLocalizations.of( - context, - ).invert, - PointerDeviceKind.unknown => AppLocalizations.of(context).error, - _ => AppLocalizations.of(context).none, - }), - ), - ListTile( - title: Text(AppLocalizations.of(context).input), - subtitle: Text('$_buttons (${_buttons.toRadixString(2)})'), - ), - ListTile( - title: Text(AppLocalizations.of(context).pressure), - subtitle: Text( - '${_pressure ?? '?'} (${_pressureMin ?? '?'} - ${_pressureMax ?? '?'})', - ), - ), - ], - ); - } -} diff --git a/app/lib/settings/inputs/keyboard.dart b/app/lib/settings/inputs/keyboard.dart index 840bf2a93f02..69be24ffaf4e 100644 --- a/app/lib/settings/inputs/keyboard.dart +++ b/app/lib/settings/inputs/keyboard.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; -import 'package:material_leap/material_leap.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import 'package:keybinder/keybinder.dart'; import 'package:butterfly/dialogs/input.dart'; @@ -13,7 +12,8 @@ import 'package:butterfly/dialogs/input.dart'; import '../../cubits/settings.dart'; class KeyboardInputSettings extends StatelessWidget { - const KeyboardInputSettings({super.key}); + final ButterflySettings state; + const KeyboardInputSettings({super.key, required this.state}); @override Widget build(BuildContext context) { @@ -51,56 +51,34 @@ class KeyboardInputSettings extends StatelessWidget { ...changeToolShortcuts, ]; - return Scaffold( - appBar: WindowTitleBar( - title: Text(AppLocalizations.of(context).keyboard), - ), - body: SingleChildScrollView( - child: Align( - alignment: Alignment.topCenter, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 1000), - child: BlocBuilder( - builder: (context, state) => ListenableBuilder( - listenable: keybinder, - builder: (context, _) => Column( - children: [ - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: ListTile( - title: Text(AppLocalizations.of(context).shortcuts), - leading: const PhosphorIcon( - PhosphorIconsLight.keyboard, - ), - onTap: () => openHelp(['shortcuts'], 'keyboard'), - trailing: const PhosphorIcon( - PhosphorIconsLight.arrowSquareOut, - ), - ), - ), - ), - const SizedBox(height: 16), - _buildHoldShortcutsSection( - context, - state.inputConfiguration, - ), - const SizedBox(height: 16), - _buildSection( - context, - AppLocalizations.of(context).general, - generalShortcuts, - ), - const SizedBox(height: 16), - _buildSection(context, 'Project', projectShortcuts), - const SizedBox(height: 16), - ], - ), + return ListenableBuilder( + listenable: keybinder, + builder: (context, _) => Column( + children: [ + Card( + margin: settingsCardMargin, + child: Padding( + padding: settingsCardPadding, + child: ListTile( + title: Text(AppLocalizations.of(context).shortcuts), + leading: const PhosphorIcon(PhosphorIconsLight.keyboard), + onTap: () => openHelp(['shortcuts'], 'keyboard'), + trailing: const PhosphorIcon(PhosphorIconsLight.arrowSquareOut), ), ), ), - ), + const SizedBox(height: 16), + _buildHoldShortcutsSection(context, state.inputConfiguration), + const SizedBox(height: 16), + _buildSection( + context, + AppLocalizations.of(context).general, + generalShortcuts, + ), + const SizedBox(height: 16), + _buildSection(context, 'Project', projectShortcuts), + const SizedBox(height: 16), + ], ), ); } diff --git a/app/lib/settings/inputs/mouse.dart b/app/lib/settings/inputs/mouse.dart index f43ab12996de..2e32bc2052b8 100644 --- a/app/lib/settings/inputs/mouse.dart +++ b/app/lib/settings/inputs/mouse.dart @@ -4,14 +4,14 @@ import 'package:butterfly/widgets/input_mapping_list_tile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; -import 'package:material_leap/material_leap.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../../cubits/settings.dart'; import 'shortcut.dart'; class MouseInputSettings extends StatelessWidget { - const MouseInputSettings({super.key}); + final ButterflySettings state; + const MouseInputSettings({super.key, required this.state}); String _getDoubleName(BuildContext context, String inputName) => '${AppLocalizations.of(context).double} $inputName'; @@ -20,240 +20,208 @@ class MouseInputSettings extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - appBar: WindowTitleBar( - title: Text(AppLocalizations.of(context).mouse), - ), - body: Align( - alignment: Alignment.center, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: LeapBreakpoints.compact), - child: BlocBuilder( - builder: (context, state) { - final config = state.inputConfiguration; - final availableShortcuts = getInputShortcutOptions(context); - return ListView( - children: [ - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - children: [ - SwitchListTile( - value: state.hideCursorWhileDrawing, - title: Text( - AppLocalizations.of( - context, - ).hideCursorWhileDrawing, - ), - secondary: const PhosphorIcon( - PhosphorIconsLight.cursorClick, - ), - onChanged: (value) => context - .read() - .changeHideCursorWhileDrawing(value), - ), - ], - ), - ), + final config = state.inputConfiguration; + final availableShortcuts = getInputShortcutOptions(context); + return Column( + children: [ + Card( + margin: settingsCardMargin, + child: Padding( + padding: settingsCardPadding, + child: Column( + children: [ + SwitchListTile( + value: state.hideCursorWhileDrawing, + title: Text( + AppLocalizations.of(context).hideCursorWhileDrawing, ), - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: settingsCardTitlePadding, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context).shortcuts, - style: TextTheme.of(context).headlineSmall, - ), - IconButton( - icon: const PhosphorIcon( - PhosphorIconsLight.sealQuestion, - ), - tooltip: AppLocalizations.of(context).help, - onPressed: () => - openHelp(['shortcuts'], 'configure'), - ), - ], - ), - ), - const SizedBox(height: 16), - InputMappingListTile( - inputName: AppLocalizations.of(context).left, - currentValue: config.leftMouse, - defaultValue: InputMappingDefault.leftMouse, - icon: const PhosphorIcon( - PhosphorIconsLight.mouseLeftClick, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(leftMouse: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getDoubleName( - context, - AppLocalizations.of(context).left, - ), - currentValue: config.doubleLeftMouseShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.mouseLeftClick, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(doubleLeftMouseShortcut: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getTripleName( - AppLocalizations.of(context).left, - ), - currentValue: config.tripleLeftMouseShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.mouseLeftClick, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(tripleLeftMouseShortcut: value), - ); - }, - ), - InputMappingListTile( - inputName: AppLocalizations.of(context).middle, - currentValue: config.middleMouse, - defaultValue: InputMappingDefault.middleMouse, - icon: const PhosphorIcon( - PhosphorIconsLight.mouseMiddleClick, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(middleMouse: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getDoubleName( - context, - AppLocalizations.of(context).middle, - ), - currentValue: config.doubleMiddleMouseShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.mouseMiddleClick, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith( - doubleMiddleMouseShortcut: value, - ), - ); - }, - ), - InputShortcutListTile( - inputName: _getTripleName( - AppLocalizations.of(context).middle, - ), - currentValue: config.tripleMiddleMouseShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.mouseMiddleClick, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith( - tripleMiddleMouseShortcut: value, - ), - ); - }, - ), - InputMappingListTile( - inputName: AppLocalizations.of(context).right, - currentValue: config.rightMouse, - defaultValue: InputMappingDefault.rightMouse, - icon: const PhosphorIcon( - PhosphorIconsLight.mouseRightClick, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(rightMouse: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getDoubleName( - context, - AppLocalizations.of(context).right, - ), - currentValue: config.doubleRightMouseShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.mouseRightClick, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith( - doubleRightMouseShortcut: value, - ), - ); - }, - ), - InputShortcutListTile( - inputName: _getTripleName( - AppLocalizations.of(context).right, - ), - currentValue: config.tripleRightMouseShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.mouseRightClick, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith( - tripleRightMouseShortcut: value, - ), - ); - }, - ), - ], + secondary: const PhosphorIcon(PhosphorIconsLight.cursorClick), + onChanged: (value) => context + .read() + .changeHideCursorWhileDrawing(value), + ), + ], + ), + ), + ), + Card( + margin: settingsCardMargin, + child: Padding( + padding: settingsCardPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: settingsCardTitlePadding, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context).shortcuts, + style: TextTheme.of(context).headlineSmall, ), - ), + IconButton( + icon: const PhosphorIcon( + PhosphorIconsLight.sealQuestion, + ), + tooltip: AppLocalizations.of(context).help, + onPressed: () => openHelp(['shortcuts'], 'configure'), + ), + ], + ), + ), + const SizedBox(height: 16), + InputMappingListTile( + inputName: AppLocalizations.of(context).left, + currentValue: config.leftMouse, + defaultValue: InputMappingDefault.leftMouse, + icon: const PhosphorIcon( + PhosphorIconsLight.mouseLeftClick, + textDirection: TextDirection.ltr, + ), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(leftMouse: value), + ); + }, + ), + InputShortcutListTile( + inputName: _getDoubleName( + context, + AppLocalizations.of(context).left, + ), + currentValue: config.doubleLeftMouseShortcut, + availableShortcuts: availableShortcuts, + icon: const PhosphorIcon( + PhosphorIconsLight.mouseLeftClick, + textDirection: TextDirection.ltr, + ), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(doubleLeftMouseShortcut: value), + ); + }, + ), + InputShortcutListTile( + inputName: _getTripleName(AppLocalizations.of(context).left), + currentValue: config.tripleLeftMouseShortcut, + availableShortcuts: availableShortcuts, + icon: const PhosphorIcon( + PhosphorIconsLight.mouseLeftClick, + textDirection: TextDirection.ltr, + ), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(tripleLeftMouseShortcut: value), + ); + }, + ), + InputMappingListTile( + inputName: AppLocalizations.of(context).middle, + currentValue: config.middleMouse, + defaultValue: InputMappingDefault.middleMouse, + icon: const PhosphorIcon( + PhosphorIconsLight.mouseMiddleClick, + textDirection: TextDirection.ltr, + ), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(middleMouse: value), + ); + }, + ), + InputShortcutListTile( + inputName: _getDoubleName( + context, + AppLocalizations.of(context).middle, + ), + currentValue: config.doubleMiddleMouseShortcut, + availableShortcuts: availableShortcuts, + icon: const PhosphorIcon( + PhosphorIconsLight.mouseMiddleClick, + textDirection: TextDirection.ltr, + ), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(doubleMiddleMouseShortcut: value), + ); + }, + ), + InputShortcutListTile( + inputName: _getTripleName( + AppLocalizations.of(context).middle, + ), + currentValue: config.tripleMiddleMouseShortcut, + availableShortcuts: availableShortcuts, + icon: const PhosphorIcon( + PhosphorIconsLight.mouseMiddleClick, + textDirection: TextDirection.ltr, + ), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(tripleMiddleMouseShortcut: value), + ); + }, + ), + InputMappingListTile( + inputName: AppLocalizations.of(context).right, + currentValue: config.rightMouse, + defaultValue: InputMappingDefault.rightMouse, + icon: const PhosphorIcon( + PhosphorIconsLight.mouseRightClick, + textDirection: TextDirection.ltr, + ), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(rightMouse: value), + ); + }, + ), + InputShortcutListTile( + inputName: _getDoubleName( + context, + AppLocalizations.of(context).right, + ), + currentValue: config.doubleRightMouseShortcut, + availableShortcuts: availableShortcuts, + icon: const PhosphorIcon( + PhosphorIconsLight.mouseRightClick, + textDirection: TextDirection.ltr, + ), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(doubleRightMouseShortcut: value), + ); + }, + ), + InputShortcutListTile( + inputName: _getTripleName(AppLocalizations.of(context).right), + currentValue: config.tripleRightMouseShortcut, + availableShortcuts: availableShortcuts, + icon: const PhosphorIcon( + PhosphorIconsLight.mouseRightClick, + textDirection: TextDirection.ltr, ), - ], - ); - }, + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(tripleRightMouseShortcut: value), + ); + }, + ), + ], + ), ), ), - ), + ], ); } } diff --git a/app/lib/settings/inputs/pen.dart b/app/lib/settings/inputs/pen.dart index ac23fe45c36c..c520ded360c1 100644 --- a/app/lib/settings/inputs/pen.dart +++ b/app/lib/settings/inputs/pen.dart @@ -11,7 +11,8 @@ import '../../cubits/settings.dart'; import 'shortcut.dart'; class PenInputSettings extends StatelessWidget { - const PenInputSettings({super.key}); + final ButterflySettings state; + const PenInputSettings({super.key, required this.state}); String _getDoubleName(BuildContext context, String inputName) => '${AppLocalizations.of(context).double} $inputName'; @@ -46,407 +47,349 @@ class PenInputSettings extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - appBar: WindowTitleBar( - title: Text(AppLocalizations.of(context).pen), - ), - body: Align( - alignment: Alignment.center, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: LeapBreakpoints.compact), - child: BlocBuilder( - builder: (context, state) { - final config = state.inputConfiguration; - final availableShortcuts = getInputShortcutOptions(context); - return ListView( - children: [ - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ + final config = state.inputConfiguration; + final availableShortcuts = getInputShortcutOptions(context); + return Column( + children: [ + Card( + margin: settingsCardMargin, + child: Padding( + padding: settingsCardPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ListTile( + title: Text(AppLocalizations.of(context).penOnlyInput), + subtitle: Text( + _getPenOnlyInputName(state.penOnlyInput, context), + ), + leading: const PhosphorIcon( + PhosphorIconsLight.pencilSimpleLine, + ), + onTap: () { + final cubit = context.read(); + final current = cubit.state.penOnlyInput; + + showLeapBottomSheet( + context: context, + titleBuilder: (context) => + Text(AppLocalizations.of(context).penOnlyInput), + childrenBuilder: (context) { + return [ ListTile( - title: Text( - AppLocalizations.of(context).penOnlyInput, - ), + title: Text(AppLocalizations.of(context).automatic), subtitle: Text( - _getPenOnlyInputName(state.penOnlyInput, context), - ), - leading: const PhosphorIcon( - PhosphorIconsLight.pencilSimpleLine, + AppLocalizations.of( + context, + ).penOnlyInputAutoDescription, ), + selected: current == null, onTap: () { - final cubit = context.read(); - final current = cubit.state.penOnlyInput; - - showLeapBottomSheet( - context: context, - titleBuilder: (context) => Text( - AppLocalizations.of(context).penOnlyInput, - ), - childrenBuilder: (context) { - return [ - ListTile( - title: Text( - AppLocalizations.of(context).automatic, - ), - subtitle: Text( - AppLocalizations.of( - context, - ).penOnlyInputAutoDescription, - ), - selected: current == null, - onTap: () { - cubit.changePenOnlyInput(null); - Navigator.of(context).pop(); - }, - ), - ListTile( - title: Text( - AppLocalizations.of(context).alwaysOn, - ), - subtitle: Text( - AppLocalizations.of( - context, - ).penOnlyInputOnDescription, - ), - selected: current == true, - onTap: () { - cubit.changePenOnlyInput(true); - Navigator.of(context).pop(); - }, - ), - ListTile( - title: Text( - AppLocalizations.of(context).alwaysOff, - ), - subtitle: Text( - AppLocalizations.of( - context, - ).penOnlyInputOffDescription, - ), - selected: current == false, - onTap: () { - cubit.changePenOnlyInput(false); - Navigator.of(context).pop(); - }, - ), - ]; - }, - ); + cubit.changePenOnlyInput(null); + Navigator.of(context).pop(); }, ), - SwitchListTile( - value: state.showPenOnlyToggle, - title: Text( - AppLocalizations.of(context).showPenOnlyToggle, - ), - secondary: const PhosphorIcon( - PhosphorIconsLight.toggleRight, - ), - onChanged: (value) => context - .read() - .changeShowPenOnlyToggle(value), - ), ListTile( - title: Text( - AppLocalizations.of(context).ignorePressure, - ), + title: Text(AppLocalizations.of(context).alwaysOn), subtitle: Text( - _getIgnorePressureName( - state.ignorePressure, + AppLocalizations.of( context, - ), - ), - leading: const PhosphorIcon( - PhosphorIconsLight.lineSegments, + ).penOnlyInputOnDescription, ), + selected: current == true, onTap: () { - final cubit = context.read(); - final ignorePressure = cubit.state.ignorePressure; - - showLeapBottomSheet( - context: context, - titleBuilder: (context) => Text( - AppLocalizations.of(context).ignorePressure, - ), - childrenBuilder: (context) { - return [ - ...IgnorePressure.values.map((e) { - final description = - _getIgnorePressureDescription( - e, - context, - ); - return ListTile( - title: Text( - _getIgnorePressureName(e, context), - ), - subtitle: description != null - ? Text(description) - : null, - selected: e == ignorePressure, - onTap: () { - cubit.changeIgnorePressure(e); - Navigator.of(context).pop(); - }, - ); - }), - ]; - }, - ); - }, - ), - ], - ), - ), - ), - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: settingsCardTitlePadding, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context).shortcuts, - style: TextTheme.of(context).headlineSmall, - ), - IconButton( - icon: const PhosphorIcon( - PhosphorIconsLight.sealQuestion, - ), - tooltip: AppLocalizations.of(context).help, - onPressed: () => - openHelp(['shortcuts'], 'configure'), - ), - ], - ), - ), - const SizedBox(height: 16), - InputMappingListTile( - inputName: AppLocalizations.of(context).pen, - currentValue: config.pen, - defaultValue: InputMappingDefault.pen, - icon: const PhosphorIcon(PhosphorIconsLight.pen), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(pen: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getDoubleName( - context, - AppLocalizations.of(context).pen, - ), - currentValue: config.doublePenShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon(PhosphorIconsLight.pen), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(doublePenShortcut: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getTripleName( - AppLocalizations.of(context).pen, - ), - currentValue: config.triplePenShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon(PhosphorIconsLight.pen), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(triplePenShortcut: value), - ); - }, - ), - InputMappingListTile( - inputName: AppLocalizations.of(context).invertedPen, - currentValue: config.invertedPen, - defaultValue: InputMappingDefault.invertedPen, - icon: Transform.flip( - flipX: true, - flipY: true, - child: PhosphorIcon(PhosphorIconsLight.pen), - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(invertedPen: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getDoubleName( - context, - AppLocalizations.of(context).invertedPen, - ), - currentValue: config.doubleInvertedPenShortcut, - availableShortcuts: availableShortcuts, - icon: Transform.flip( - flipX: true, - flipY: true, - child: PhosphorIcon(PhosphorIconsLight.pen), - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith( - doubleInvertedPenShortcut: value, - ), - ); - }, - ), - InputShortcutListTile( - inputName: _getTripleName( - AppLocalizations.of(context).invertedPen, - ), - currentValue: config.tripleInvertedPenShortcut, - availableShortcuts: availableShortcuts, - icon: Transform.flip( - flipX: true, - flipY: true, - child: PhosphorIcon(PhosphorIconsLight.pen), - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith( - tripleInvertedPenShortcut: value, - ), - ); - }, - ), - InputMappingListTile( - inputName: AppLocalizations.of(context).first, - currentValue: config.firstPenButton, - defaultValue: InputMappingDefault.firstPenButton, - icon: const PhosphorIcon( - PhosphorIconsLight.numberCircleOne, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(firstPenButton: value), - ); + cubit.changePenOnlyInput(true); + Navigator.of(context).pop(); }, ), - InputShortcutListTile( - inputName: _getDoubleName( - context, - AppLocalizations.of(context).first, - ), - currentValue: config.doubleFirstPenButtonShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.numberCircleOne, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith( - doubleFirstPenButtonShortcut: value, - ), - ); - }, - ), - InputShortcutListTile( - inputName: _getTripleName( - AppLocalizations.of(context).first, - ), - currentValue: config.tripleFirstPenButtonShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.numberCircleOne, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith( - tripleFirstPenButtonShortcut: value, - ), - ); - }, - ), - InputMappingListTile( - inputName: AppLocalizations.of(context).second, - currentValue: config.secondPenButton, - defaultValue: InputMappingDefault.secondPenButton, - icon: const PhosphorIcon( - PhosphorIconsLight.numberCircleTwo, - textDirection: TextDirection.ltr, + ListTile( + title: Text(AppLocalizations.of(context).alwaysOff), + subtitle: Text( + AppLocalizations.of( + context, + ).penOnlyInputOffDescription, ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(secondPenButton: value), - ); + selected: current == false, + onTap: () { + cubit.changePenOnlyInput(false); + Navigator.of(context).pop(); }, ), - InputShortcutListTile( - inputName: _getDoubleName( + ]; + }, + ); + }, + ), + SwitchListTile( + value: state.showPenOnlyToggle, + title: Text(AppLocalizations.of(context).showPenOnlyToggle), + secondary: const PhosphorIcon(PhosphorIconsLight.toggleRight), + onChanged: (value) => context + .read() + .changeShowPenOnlyToggle(value), + ), + ListTile( + title: Text(AppLocalizations.of(context).ignorePressure), + subtitle: Text( + _getIgnorePressureName(state.ignorePressure, context), + ), + leading: const PhosphorIcon(PhosphorIconsLight.lineSegments), + onTap: () { + final cubit = context.read(); + final ignorePressure = cubit.state.ignorePressure; + + showLeapBottomSheet( + context: context, + titleBuilder: (context) => + Text(AppLocalizations.of(context).ignorePressure), + childrenBuilder: (context) { + return [ + ...IgnorePressure.values.map((e) { + final description = _getIgnorePressureDescription( + e, context, - AppLocalizations.of(context).second, - ), - currentValue: config.doubleSecondPenButtonShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.numberCircleTwo, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith( - doubleSecondPenButtonShortcut: value, - ), - ); - }, - ), - InputShortcutListTile( - inputName: _getTripleName( - AppLocalizations.of(context).second, - ), - currentValue: config.tripleSecondPenButtonShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.numberCircleTwo, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith( - tripleSecondPenButtonShortcut: value, - ), - ); - }, - ), - ], + ); + return ListTile( + title: Text(_getIgnorePressureName(e, context)), + subtitle: description != null + ? Text(description) + : null, + selected: e == ignorePressure, + onTap: () { + cubit.changeIgnorePressure(e); + Navigator.of(context).pop(); + }, + ); + }), + ]; + }, + ); + }, + ), + ], + ), + ), + ), + Card( + margin: settingsCardMargin, + child: Padding( + padding: settingsCardPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: settingsCardTitlePadding, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context).shortcuts, + style: TextTheme.of(context).headlineSmall, + ), + IconButton( + icon: const PhosphorIcon( + PhosphorIconsLight.sealQuestion, + ), + tooltip: AppLocalizations.of(context).help, + onPressed: () => openHelp(['shortcuts'], 'configure'), ), - ), + ], + ), + ), + const SizedBox(height: 16), + InputMappingListTile( + inputName: AppLocalizations.of(context).pen, + currentValue: config.pen, + defaultValue: InputMappingDefault.pen, + icon: const PhosphorIcon(PhosphorIconsLight.pen), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration(config.copyWith(pen: value)); + }, + ), + InputShortcutListTile( + inputName: _getDoubleName( + context, + AppLocalizations.of(context).pen, + ), + currentValue: config.doublePenShortcut, + availableShortcuts: availableShortcuts, + icon: const PhosphorIcon(PhosphorIconsLight.pen), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(doublePenShortcut: value), + ); + }, + ), + InputShortcutListTile( + inputName: _getTripleName(AppLocalizations.of(context).pen), + currentValue: config.triplePenShortcut, + availableShortcuts: availableShortcuts, + icon: const PhosphorIcon(PhosphorIconsLight.pen), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(triplePenShortcut: value), + ); + }, + ), + InputMappingListTile( + inputName: AppLocalizations.of(context).invertedPen, + currentValue: config.invertedPen, + defaultValue: InputMappingDefault.invertedPen, + icon: Transform.flip( + flipX: true, + flipY: true, + child: PhosphorIcon(PhosphorIconsLight.pen), + ), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(invertedPen: value), + ); + }, + ), + InputShortcutListTile( + inputName: _getDoubleName( + context, + AppLocalizations.of(context).invertedPen, + ), + currentValue: config.doubleInvertedPenShortcut, + availableShortcuts: availableShortcuts, + icon: Transform.flip( + flipX: true, + flipY: true, + child: PhosphorIcon(PhosphorIconsLight.pen), + ), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(doubleInvertedPenShortcut: value), + ); + }, + ), + InputShortcutListTile( + inputName: _getTripleName( + AppLocalizations.of(context).invertedPen, + ), + currentValue: config.tripleInvertedPenShortcut, + availableShortcuts: availableShortcuts, + icon: Transform.flip( + flipX: true, + flipY: true, + child: PhosphorIcon(PhosphorIconsLight.pen), + ), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(tripleInvertedPenShortcut: value), + ); + }, + ), + InputMappingListTile( + inputName: AppLocalizations.of(context).first, + currentValue: config.firstPenButton, + defaultValue: InputMappingDefault.firstPenButton, + icon: const PhosphorIcon( + PhosphorIconsLight.numberCircleOne, + textDirection: TextDirection.ltr, + ), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(firstPenButton: value), + ); + }, + ), + InputShortcutListTile( + inputName: _getDoubleName( + context, + AppLocalizations.of(context).first, + ), + currentValue: config.doubleFirstPenButtonShortcut, + availableShortcuts: availableShortcuts, + icon: const PhosphorIcon( + PhosphorIconsLight.numberCircleOne, + textDirection: TextDirection.ltr, + ), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(doubleFirstPenButtonShortcut: value), + ); + }, + ), + InputShortcutListTile( + inputName: _getTripleName(AppLocalizations.of(context).first), + currentValue: config.tripleFirstPenButtonShortcut, + availableShortcuts: availableShortcuts, + icon: const PhosphorIcon( + PhosphorIconsLight.numberCircleOne, + textDirection: TextDirection.ltr, + ), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(tripleFirstPenButtonShortcut: value), + ); + }, + ), + InputMappingListTile( + inputName: AppLocalizations.of(context).second, + currentValue: config.secondPenButton, + defaultValue: InputMappingDefault.secondPenButton, + icon: const PhosphorIcon( + PhosphorIconsLight.numberCircleTwo, + textDirection: TextDirection.ltr, + ), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(secondPenButton: value), + ); + }, + ), + InputShortcutListTile( + inputName: _getDoubleName( + context, + AppLocalizations.of(context).second, + ), + currentValue: config.doubleSecondPenButtonShortcut, + availableShortcuts: availableShortcuts, + icon: const PhosphorIcon( + PhosphorIconsLight.numberCircleTwo, + textDirection: TextDirection.ltr, + ), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(doubleSecondPenButtonShortcut: value), + ); + }, + ), + InputShortcutListTile( + inputName: _getTripleName( + AppLocalizations.of(context).second, + ), + currentValue: config.tripleSecondPenButtonShortcut, + availableShortcuts: availableShortcuts, + icon: const PhosphorIcon( + PhosphorIconsLight.numberCircleTwo, + textDirection: TextDirection.ltr, ), - ], - ); - }, + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(tripleSecondPenButtonShortcut: value), + ); + }, + ), + ], + ), ), ), - ), + ], ); } } diff --git a/app/lib/settings/inputs/touch.dart b/app/lib/settings/inputs/touch.dart index 7565f52a0fae..72283c6a075e 100644 --- a/app/lib/settings/inputs/touch.dart +++ b/app/lib/settings/inputs/touch.dart @@ -3,7 +3,6 @@ import 'package:butterfly/widgets/input_mapping_list_tile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; -import 'package:material_leap/material_leap.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../../api/open.dart'; @@ -11,146 +10,109 @@ import '../../cubits/settings.dart'; import 'shortcut.dart'; class TouchInputSettings extends StatelessWidget { - const TouchInputSettings({super.key}); + final ButterflySettings state; + const TouchInputSettings({super.key, required this.state}); @override Widget build(BuildContext context) { final availableShortcuts = getInputShortcutOptions(context); - return Scaffold( - appBar: WindowTitleBar( - title: Text(AppLocalizations.of(context).touch), - ), - body: Align( - alignment: Alignment.center, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: LeapBreakpoints.compact), - child: BlocBuilder( - builder: (context, state) { - final config = state.inputConfiguration; - return ListView( - children: [ - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SwitchListTile( - value: state.inputGestures, - title: Text( - AppLocalizations.of(context).inputGestures, - ), - secondary: const PhosphorIcon( - PhosphorIconsLight.handTap, - ), - onChanged: (value) => context - .read() - .changeInputGestures(value), - ), - SwitchListTile( - value: state.moveOnGesture, - title: Text( - AppLocalizations.of(context).moveOnGesture, - ), - secondary: const PhosphorIcon( - PhosphorIconsLight.arrowsOutCardinal, - ), - onChanged: (value) => context - .read() - .changeMoveOnGesture(value), - ), - ], - ), - ), + final config = state.inputConfiguration; + return Column( + children: [ + Card( + margin: settingsCardMargin, + child: Padding( + padding: settingsCardPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SwitchListTile( + value: state.inputGestures, + title: Text(AppLocalizations.of(context).inputGestures), + secondary: const PhosphorIcon(PhosphorIconsLight.handTap), + onChanged: (value) => + context.read().changeInputGestures(value), + ), + SwitchListTile( + value: state.moveOnGesture, + title: Text(AppLocalizations.of(context).moveOnGesture), + secondary: const PhosphorIcon( + PhosphorIconsLight.arrowsOutCardinal, ), - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: settingsCardTitlePadding, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context).shortcuts, - style: TextTheme.of(context).headlineSmall, - ), - IconButton( - icon: const PhosphorIcon( - PhosphorIconsLight.sealQuestion, - ), - tooltip: AppLocalizations.of(context).help, - onPressed: () => - openHelp(['shortcuts'], 'configure'), - ), - ], - ), - ), - const SizedBox(height: 16), - InputMappingListTile( - inputName: AppLocalizations.of(context).touch, - currentValue: config.touch, - defaultValue: InputMappingDefault.touch, - icon: const PhosphorIcon( - PhosphorIconsLight.handPointing, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(touch: value), - ); - }, - ), - InputShortcutListTile( - inputName: AppLocalizations.of( - context, - ).doublePressAction, - currentValue: config.doubleTouchShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.handTap, - ), - onChanged: (value) { - context - .read() - .changeInputConfiguration( - config.copyWith(doubleTouchShortcut: value), - ); - }, - ), - InputShortcutListTile( - inputName: AppLocalizations.of( - context, - ).triplePressAction, - currentValue: config.tripleTouchShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.handTap, - ), - onChanged: (value) { - context - .read() - .changeInputConfiguration( - config.copyWith(tripleTouchShortcut: value), - ); - }, - ), - ], + onChanged: (value) => + context.read().changeMoveOnGesture(value), + ), + ], + ), + ), + ), + Card( + margin: settingsCardMargin, + child: Padding( + padding: settingsCardPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: settingsCardTitlePadding, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context).shortcuts, + style: TextTheme.of(context).headlineSmall, + ), + IconButton( + icon: const PhosphorIcon( + PhosphorIconsLight.sealQuestion, + ), + tooltip: AppLocalizations.of(context).help, + onPressed: () => openHelp(['shortcuts'], 'configure'), ), - ), + ], ), - ], - ); - }, + ), + const SizedBox(height: 16), + InputMappingListTile( + inputName: AppLocalizations.of(context).touch, + currentValue: config.touch, + defaultValue: InputMappingDefault.touch, + icon: const PhosphorIcon(PhosphorIconsLight.handPointing), + onChanged: (value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + config.copyWith(touch: value), + ); + }, + ), + InputShortcutListTile( + inputName: AppLocalizations.of(context).doublePressAction, + currentValue: config.doubleTouchShortcut, + availableShortcuts: availableShortcuts, + icon: const PhosphorIcon(PhosphorIconsLight.handTap), + onChanged: (value) { + context.read().changeInputConfiguration( + config.copyWith(doubleTouchShortcut: value), + ); + }, + ), + InputShortcutListTile( + inputName: AppLocalizations.of(context).triplePressAction, + currentValue: config.tripleTouchShortcut, + availableShortcuts: availableShortcuts, + icon: const PhosphorIcon(PhosphorIconsLight.handTap), + onChanged: (value) { + context.read().changeInputConfiguration( + config.copyWith(tripleTouchShortcut: value), + ); + }, + ), + ], + ), ), ), - ), + ], ); } } diff --git a/app/lib/settings/logs.dart b/app/lib/settings/logs.dart deleted file mode 100644 index 7440ccadc7ba..000000000000 --- a/app/lib/settings/logs.dart +++ /dev/null @@ -1,256 +0,0 @@ -import 'dart:io'; -import 'package:butterfly/cubits/settings.dart'; -import 'package:butterfly/services/logger.dart'; -import 'package:butterfly/src/generated/i18n/app_localizations.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:intl/intl.dart'; -import 'package:material_leap/material_leap.dart'; -import 'package:phosphor_flutter/phosphor_flutter.dart'; -import 'package:talker_flutter/talker_flutter.dart'; - -class LogsSettingsPage extends StatefulWidget { - final bool inView; - const LogsSettingsPage({super.key, this.inView = false}); - - @override - State createState() => _LogsSettingsPageState(); -} - -class _LogsSettingsPageState extends State { - List _archivedFiles = []; - File? _selectedFile; - List _selectedArchivedLogs = []; - - @override - void initState() { - super.initState(); - _loadArchiveList(); - } - - Future _loadArchiveList() async { - final archives = await getArchivedLogs(); - setState(() { - _archivedFiles = archives; - }); - } - - Future _selectArchive(File? file) async { - if (file == null) { - setState(() { - _selectedFile = null; - _selectedArchivedLogs = []; - }); - return; - } - final logs = await loadArchivedLogFile(file); - setState(() { - _selectedFile = file; - _selectedArchivedLogs = logs; - }); - } - - String _getFileName(File file) { - try { - final name = file.path.split(Platform.pathSeparator).last; - return name.replaceAll('logs_', '').replaceAll('.json', ''); - } catch (_) { - return 'Archive'; - } - } - - Widget _buildList(List allLogs, bool showVerbose) { - final logs = allLogs.where((element) { - if (showVerbose) return true; - return element.logLevel != LogLevel.verbose && - element.logLevel != LogLevel.debug; - }).toList(); - if (logs.isEmpty) { - return const Center(child: Text('No logs')); - } - return ListView.builder( - itemCount: logs.length, - itemBuilder: (context, index) { - final log = logs[logs.length - 1 - index]; - return _LogTile(log: log); - }, - ); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - talker.configure( - settings: talker.settings.copyWith( - useConsoleLogs: state.showVerboseLogs, - ), - ); - return Scaffold( - backgroundColor: widget.inView ? Colors.transparent : null, - appBar: WindowTitleBar( - title: Text(AppLocalizations.of(context).logs), - backgroundColor: widget.inView ? Colors.transparent : null, - inView: widget.inView, - actions: [ - IconButton( - icon: const PhosphorIcon(PhosphorIconsLight.copy), - tooltip: AppLocalizations.of(context).copy, - onPressed: () { - final sourceLogs = _selectedFile == null - ? talker.history - : _selectedArchivedLogs; - final text = sourceLogs - .map((e) => e.generateTextMessage()) - .join('\n'); - Clipboard.setData(ClipboardData(text: text)); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context).copyTitle), - ), - ); - }, - ), - IconButton( - icon: const PhosphorIcon(PhosphorIconsLight.trash), - tooltip: AppLocalizations.of(context).delete, - onPressed: () { - talker.cleanHistory(); - clearPersistedLogs(); - _loadArchiveList(); - _selectArchive(null); - setState(() {}); - }, - ), - ], - ), - body: Column( - children: [ - if (_archivedFiles.isNotEmpty) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: DropdownMenuFormField( - initialSelection: _selectedFile, - expandedInsets: EdgeInsets.zero, - dropdownMenuEntries: [ - DropdownMenuEntry( - value: null, - label: AppLocalizations.of(context).currentVersion, - ), - ..._archivedFiles.map( - (file) => DropdownMenuEntry( - value: file, - label: _getFileName(file), - ), - ), - ], - onSelected: _selectArchive, - ), - ), - SwitchListTile( - title: const Text('Show verbose logs'), - value: state.showVerboseLogs, - onChanged: (value) { - context.read().changeShowVerboseLogs(value); - }, - ), - Expanded( - child: _selectedFile == null - ? StreamBuilder( - stream: talker.stream, - builder: (context, snapshot) { - return _buildList( - talker.history, - state.showVerboseLogs, - ); - }, - ) - : _buildList(_selectedArchivedLogs, state.showVerboseLogs), - ), - ], - ), - ); - }, - ); - } -} - -class _LogTile extends StatelessWidget { - final TalkerData log; - const _LogTile({required this.log}); - - @override - Widget build(BuildContext context) { - Color? color; - switch (log.logLevel) { - case LogLevel.error: - case LogLevel.critical: - color = Colors.red; - break; - case LogLevel.warning: - color = Colors.orange; - break; - case LogLevel.verbose: - case LogLevel.debug: - color = Colors.grey; - break; - case LogLevel.info: - default: - break; - } - - return ListTile( - title: Text( - log.generateTextMessage(), - style: TextStyle(color: color), - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text(DateFormat('HH:mm:ss').format(log.time)), - trailing: IconButton( - icon: const PhosphorIcon(PhosphorIconsLight.copy), - onPressed: () { - Clipboard.setData(ClipboardData(text: log.generateTextMessage())); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(AppLocalizations.of(context).copyTitle)), - ); - }, - ), - onTap: () { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(log.title ?? 'Log'), - content: SingleChildScrollView( - child: SelectableText(log.generateTextMessage()), - ), - actions: [ - TextButton( - onPressed: () { - Clipboard.setData( - ClipboardData(text: log.generateTextMessage()), - ); - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context).copyTitle), - ), - ); - }, - child: Text(AppLocalizations.of(context).copy), - ), - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Close'), - ), - ], - ), - ); - }, - ); - } -} diff --git a/app/lib/settings/pages/behaviors/home.dart b/app/lib/settings/pages/behaviors/home.dart new file mode 100644 index 000000000000..2cc0b15dbbee --- /dev/null +++ b/app/lib/settings/pages/behaviors/home.dart @@ -0,0 +1,134 @@ +part of '../../home.dart'; + +final _behaviorsSettingsPage = SettingsLeapPage( + id: 'behaviors', + displayName: (context) => AppLocalizations.of(context).behaviors, + icon: PhosphorIconsLight.faders, + appBarBuilder: _butterflyAppBar, + children: {'persistence': _persistenceSettingsPage}, + sections: { + 'behavior': SettingsLeapSection( + settings: [ + SettingsLeapCustomSetting( + displayName: (context) => AppLocalizations.of(context).autosave, + keywordsBuilder: (context) => [AppLocalizations.of(context).save], + builder: _autosaveSetting, + ), + SettingsLeapCustomSetting( + displayName: (context) => AppLocalizations.of(context).autosaveDelay, + enabled: (context, state) => state.autosave && state.delayedAutosave, + builder: _autosaveDelaySetting, + ), + SettingsLeapEnumSetting( + displayName: (context) => AppLocalizations.of(context).onStartup, + icon: PhosphorIconsLight.arrowFatLineUp, + values: StartupBehavior.values, + read: (state) => state.onStartup, + write: (context, value) => + context.read().changeStartupBehavior(value), + valueLabel: _startupBehaviorName, + ), + SettingsLeapActionSetting( + displayName: (context) => + AppLocalizations.of(context).persistenceDocumentStates, + icon: PhosphorIconsLight.database, + onTap: _openPersistenceSettings, + ), + SettingsLeapBoolSetting( + displayName: (context) => + AppLocalizations.of(context).startInFullScreen, + icon: PhosphorIconsLight.arrowsOut, + read: (state) => state.startInFullScreen, + write: (context, value) => + context.read().changeStartInFullScreen(value), + ), + SettingsLeapCustomSetting( + displayName: (context) => + AppLocalizations.of(context).contentViewport, + builder: _contentViewportSetting, + ), + SettingsLeapBoolSetting( + displayName: (context) => + AppLocalizations.of(context).limitViewportToPositiveCoordinates, + icon: PhosphorIconsLight.plusSquare, + read: (state) => state.limitViewportPositive, + write: (context, value) => + context.read().changeLimitViewportPositive(value), + ), + SettingsLeapEnumSetting( + displayName: (context) => + AppLocalizations.of(context).renderResolution, + icon: PhosphorIconsLight.sparkle, + values: RenderResolution.values, + read: (state) => state.renderResolution, + write: (context, value) => + context.read().changeRenderResolution(value), + valueLabel: _renderResolutionName, + ), + SettingsLeapBoolSetting( + displayName: (context) => + AppLocalizations.of(context).bringMovedElementsToFront, + icon: PhosphorIconsLight.stack, + read: (state) => state.bringMovedElementsToFront, + write: (context, value) => context + .read() + .changeBringMovedElementsToFront(value), + ), + ], + ), + 'import': SettingsLeapSection( + displayName: (context) => AppLocalizations.of(context).import, + settings: [ + SettingsLeapBoolSetting( + displayName: (context) => AppLocalizations.of(context).spreadToPages, + icon: PhosphorIconsLight.arrowsOutSimple, + read: (state) => state.spreadPages, + write: (context, value) => + context.read().changeSpreadPages(value), + ), + SettingsLeapCustomSetting( + displayName: (context) => AppLocalizations.of(context).imageScale, + builder: _imageScaleSetting, + ), + ], + ), + }, +); + +String _renderResolutionName(BuildContext context, RenderResolution value) => + switch (value) { + RenderResolution.performance => AppLocalizations.of(context).performance, + RenderResolution.normal => AppLocalizations.of(context).normal, + RenderResolution.high => AppLocalizations.of(context).high, + }; + +Widget _autosaveSetting(BuildContext context, ButterflySettings state) { + return ListTile( + title: Text(AppLocalizations.of(context).autosave), + leading: const PhosphorIcon(PhosphorIconsLight.floppyDisk), + subtitle: Text( + state.autosave + ? state.delayedAutosave + ? AppLocalizations.of(context).delay + : state.showSaveButton + ? AppLocalizations.of(context).yesButShowButtons + : AppLocalizations.of(context).yes + : AppLocalizations.of(context).no, + ), + onTap: () => _openAutosaveModal(context), + ); +} + +Widget _autosaveDelaySetting(BuildContext context, ButterflySettings state) { + return ExactSlider( + leading: const PhosphorIcon(PhosphorIconsLight.clock), + header: Text(AppLocalizations.of(context).autosaveDelay), + value: state.autosaveDelaySeconds.toDouble(), + min: 1, + max: 10, + defaultValue: 3, + fractionDigits: 0, + onChangeEnd: (value) => + context.read().changeAutosaveDelaySeconds(value.toInt()), + ); +} diff --git a/app/lib/settings/pages/behaviors/persistence.dart b/app/lib/settings/pages/behaviors/persistence.dart new file mode 100644 index 000000000000..fccb775c4dbb --- /dev/null +++ b/app/lib/settings/pages/behaviors/persistence.dart @@ -0,0 +1,274 @@ +part of '../../home.dart'; + +final _persistenceSettingsPage = SettingsLeapPage( + id: 'persistence', + displayName: (context) => + AppLocalizations.of(context).persistenceDocumentStates, + icon: PhosphorIconsLight.database, + appBarBuilder: _butterflyAppBar, + builder: _buildPersistenceSettingsPage, +); + +Widget _buildPersistenceSettingsPage( + BuildContext context, + ButterflySettings state, + bool inView, +) { + final settings = state.documentStatePersistence; + void change(DocumentStatePersistenceSettings next) { + context.read().changeDocumentStatePersistence(next); + } + + return ListView( + children: [ + SwitchListTile( + value: settings.enabled, + secondary: const PhosphorIcon(PhosphorIconsLight.power), + title: Text(AppLocalizations.of(context).persistentStatesEnabled), + onChanged: (value) => change(settings.copyWith(enabled: value)), + ), + const Divider(), + SwitchListTile( + value: settings.page, + secondary: const PhosphorIcon(PhosphorIconsLight.file), + title: Text(AppLocalizations.of(context).persistentStateCurrentPage), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(page: value)) + : null, + ), + SwitchListTile( + value: settings.camera, + secondary: const PhosphorIcon(PhosphorIconsLight.frameCorners), + title: Text(AppLocalizations.of(context).persistentStateViewport), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(camera: value)) + : null, + ), + SwitchListTile( + value: settings.locks, + secondary: const PhosphorIcon(PhosphorIconsLight.lockKey), + title: Text(AppLocalizations.of(context).lock), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(locks: value)) + : null, + ), + SwitchListTile( + value: settings.tool, + secondary: const PhosphorIcon(PhosphorIconsLight.toolbox), + title: Text(AppLocalizations.of(context).persistentStateSelectedTool), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(tool: value)) + : null, + ), + SwitchListTile( + value: settings.navigator, + secondary: const PhosphorIcon(PhosphorIconsLight.sidebar), + title: Text(AppLocalizations.of(context).navigator), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(navigator: value)) + : null, + ), + SwitchListTile( + value: settings.layers, + secondary: const PhosphorIcon(PhosphorIconsLight.stack), + title: Text(AppLocalizations.of(context).layers), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(layers: value)) + : null, + ), + SwitchListTile( + value: settings.areas, + secondary: const PhosphorIcon(PhosphorIconsLight.selection), + title: Text(AppLocalizations.of(context).areas), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(areas: value)) + : null, + ), + const Divider(), + ExactSlider( + header: Text(AppLocalizations.of(context).persistentStateMaxRecords), + leading: const PhosphorIcon(PhosphorIconsLight.listNumbers), + value: settings.maxEntries.toDouble(), + min: 20, + max: 2000, + defaultValue: 400, + fractionDigits: 0, + onChangeEnd: (value) => + change(settings.copyWith(maxEntries: value.toInt())), + ), + ExactSlider( + header: Text( + AppLocalizations.of(context).persistentStateDeleteOlderThanDays, + ), + leading: const PhosphorIcon(PhosphorIconsLight.calendar), + value: settings.maxAgeDays.toDouble(), + min: 7, + max: 730, + defaultValue: 180, + fractionDigits: 0, + onChangeEnd: (value) => + change(settings.copyWith(maxAgeDays: value.toInt())), + ), + ListTile( + leading: const PhosphorIcon(PhosphorIconsLight.trash), + title: Text(AppLocalizations.of(context).persistentStateCleanup), + subtitle: Text( + AppLocalizations.of(context).persistentStateCleanupDescription, + ), + enabled: settings.enabled, + onTap: settings.enabled + ? () => _cleanupPersistentStates(context) + : null, + ), + ], + ); +} + +Future _cleanupPersistentStates(BuildContext context) async { + final messenger = ScaffoldMessenger.of(context); + final settingsCubit = context.read(); + final fileSystem = context.read(); + final result = await _showCleanupTargetsDialog( + context, + settingsCubit, + fileSystem, + ); + if (result == null) return; + if (!context.mounted) return; + messenger.showSnackBar( + SnackBar( + content: Text( + AppLocalizations.of(context).persistentStateCleanupFeedback( + result.targetCount, + result.deletedRecords, + ), + ), + ), + ); +} + +Future<_CleanupResult?> _showCleanupTargetsDialog( + BuildContext context, + SettingsCubit settingsCubit, + ButterflyFileSystem fileSystem, +) async { + final settings = settingsCubit.state; + final targets = [ + _CleanupTarget( + id: '', + label: AppLocalizations.of(context).local, + storage: null, + ), + ...settings.connections.map( + (connection) => _CleanupTarget( + id: connection.identifier, + label: connection.identifier, + storage: connection, + ), + ), + ]; + final selected = targets.map((target) => target.id).toSet(); + var cleaning = false; + + return showDialog<_CleanupResult>( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setState) => AlertDialog( + title: Text(AppLocalizations.of(context).persistentStateCleanup), + scrollable: true, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (cleaning) ...[ + const LinearProgressIndicator(), + const SizedBox(height: 16), + ], + for (final target in targets) + CheckboxListTile( + value: selected.contains(target.id), + onChanged: cleaning + ? null + : (value) { + setState(() { + if (value ?? false) { + selected.add(target.id); + } else { + selected.remove(target.id); + } + }); + }, + secondary: PhosphorIcon( + target.storage == null + ? PhosphorIconsLight.house + : PhosphorIconsLight.cloud, + ), + title: Text(target.label), + controlAffinity: ListTileControlAffinity.leading, + ), + ], + ), + actions: [ + TextButton( + onPressed: cleaning ? null : () => Navigator.of(context).pop(), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), + ), + ElevatedButton( + onPressed: selected.isEmpty || cleaning + ? null + : () async { + final selectedTargets = targets + .where((target) => selected.contains(target.id)) + .toList(); + setState(() => cleaning = true); + var deleted = 0; + for (final target in selectedTargets) { + final repository = DocumentStateRepository( + fileSystem.buildDocumentStateSystem(target.storage), + settingsProvider: () => + settingsCubit.state.documentStatePersistence, + ); + deleted += await repository.cleanup(); + } + if (!context.mounted) return; + Navigator.of(context).pop( + _CleanupResult( + targetCount: selectedTargets.length, + deletedRecords: deleted, + ), + ); + }, + child: cleaning + ? Text( + AppLocalizations.of( + context, + ).persistentStateCleanupInProgress, + ) + : Text(AppLocalizations.of(context).delete), + ), + ], + ), + ), + ); +} + +class _CleanupTarget { + const _CleanupTarget({ + required this.id, + required this.label, + required this.storage, + }); + + final String id; + final String label; + final ExternalStorage? storage; +} + +class _CleanupResult { + const _CleanupResult({ + required this.targetCount, + required this.deletedRecords, + }); + + final int targetCount; + final int deletedRecords; +} diff --git a/app/lib/settings/connections.dart b/app/lib/settings/pages/connections.dart similarity index 80% rename from app/lib/settings/connections.dart rename to app/lib/settings/pages/connections.dart index f88aa6b65a52..4b0da70f9432 100644 --- a/app/lib/settings/connections.dart +++ b/app/lib/settings/pages/connections.dart @@ -1,151 +1,123 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:math' as math; -import 'dart:ui'; - -import 'package:butterfly/api/open.dart'; -import 'package:butterfly/cubits/settings.dart'; -import 'package:butterfly/visualizer/connection.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:butterfly/src/generated/i18n/app_localizations.dart'; -import 'package:go_router/go_router.dart'; -import 'package:html/parser.dart' as html_parser; -import 'package:image/image.dart' as img; -import 'package:lw_file_system/lw_file_system.dart'; -import 'package:material_leap/material_leap.dart'; -import 'package:phosphor_flutter/phosphor_flutter.dart'; - -class ConnectionsSettingsPage extends StatelessWidget { - final bool inView; - const ConnectionsSettingsPage({super.key, this.inView = false}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: inView ? Colors.transparent : null, - appBar: WindowTitleBar( - title: Text(AppLocalizations.of(context).connections), - backgroundColor: inView ? Colors.transparent : null, - inView: inView, - actions: [ - IconButton( - icon: const PhosphorIcon(PhosphorIconsLight.sealQuestion), - tooltip: AppLocalizations.of(context).help, - onPressed: () => openHelp(['storage'], 'remote'), - ), - BlocBuilder( - builder: (context, settings) { - return IconButton( - icon: settings.defaultRemote.isEmpty - ? const PhosphorIcon(PhosphorIconsFill.house) - : const PhosphorIcon(PhosphorIconsLight.house), - tooltip: settings.defaultRemote.isEmpty - ? AppLocalizations.of(context).defaultConnection - : AppLocalizations.of(context).notDefaultConnection, - onPressed: () { - BlocProvider.of(context).setDefaultRemote(''); +part of '../home.dart'; + +final _connectionsSettingsPage = SettingsLeapPage( + id: 'connections', + displayName: (context) => AppLocalizations.of(context).connections, + icon: PhosphorIconsLight.cloud, + enabled: (context, state) => !kIsWeb, + appBarBuilder: _butterflyAppBar, + actionsBuilder: buildConnectionsSettingsActions, + floatingActionButtonBuilder: buildConnectionsSettingsFloatingActionButton, + sections: { + 'content': SettingsLeapSection( + builder: (context, state, inView) { + if (kIsWeb) { + return Center( + child: Text(AppLocalizations.of(context).webNotSupported), + ); + } + if (state.connections.isEmpty) { + return Center( + child: Text(AppLocalizations.of(context).noConnections), + ); + } + return Material( + type: MaterialType.transparency, + child: ListView.builder( + itemCount: state.connections.length, + itemBuilder: (context, index) { + final remote = state.connections[index]; + return Dismissible( + key: Key(remote.identifier), + onDismissed: (details) { + BlocProvider.of( + context, + ).deleteRemote(remote.identifier); }, + child: ListTile( + title: Text(remote.label), + leading: remote.icon?.isEmpty ?? true + ? PhosphorIcon(remote.typeIcon(PhosphorIconsStyle.light)) + : Image.memory(remote.icon!), + onTap: () => context.pushNamed( + 'connection', + pathParameters: {'id': remote.identifier}, + ), + trailing: IconButton( + icon: remote.identifier == state.defaultRemote + ? const PhosphorIcon(PhosphorIconsFill.cloud) + : const PhosphorIcon(PhosphorIconsLight.cloud), + tooltip: remote.identifier == state.defaultRemote + ? AppLocalizations.of(context).defaultConnection + : AppLocalizations.of(context).notDefaultConnection, + onPressed: () { + BlocProvider.of( + context, + ).setDefaultRemote(remote.identifier); + }, + ), + ), ); }, ), - ], - ), - floatingActionButton: kIsWeb - ? null - : FloatingActionButton.extended( - onPressed: () => - showLeapBottomSheet( - context: context, - titleBuilder: (context) => - Text(AppLocalizations.of(context).addConnection), - childrenBuilder: (context) => getSupportedStorages() - .map( - (e) => ListTile( - title: Text(e.getLocalizedTypeName(context)), - leading: PhosphorIcon( - e.typeIcon(PhosphorIconsStyle.light), - ), - onTap: () => Navigator.pop(context, e), - ), - ) - .toList(), - ).then((value) { - if (value == null) return; - showDialog( - context: context, - builder: (context) => _AddRemoteDialog(storage: value), - ); - }), - label: Text(AppLocalizations.of(context).addConnection), - icon: const PhosphorIcon(PhosphorIconsLight.plus), - ), - body: Builder( - builder: (context) { - if (kIsWeb) { - return Center( - child: Text(AppLocalizations.of(context).webNotSupported), - ); - } - return BlocBuilder( - builder: (context, state) { - if (state.connections.isEmpty) { - return Center( - child: Text(AppLocalizations.of(context).noConnections), - ); - } - return Material( - type: MaterialType.transparency, - child: ListView.builder( - itemCount: state.connections.length, - itemBuilder: (context, index) { - final remote = state.connections[index]; - return Dismissible( - key: Key(remote.identifier), - onDismissed: (details) { - BlocProvider.of( - context, - ).deleteRemote(remote.identifier); - }, - child: ListTile( - title: Text(remote.label), - leading: remote.icon?.isEmpty ?? true - ? PhosphorIcon( - remote.typeIcon(PhosphorIconsStyle.light), - ) - : Image.memory(remote.icon!), - onTap: () => context.pushNamed( - 'connection', - pathParameters: {'id': remote.identifier}, - ), - trailing: IconButton( - icon: remote.identifier == state.defaultRemote - ? const PhosphorIcon(PhosphorIconsFill.cloud) - : const PhosphorIcon(PhosphorIconsLight.cloud), - tooltip: remote.identifier == state.defaultRemote - ? AppLocalizations.of(context).defaultConnection - : AppLocalizations.of( - context, - ).notDefaultConnection, - onPressed: () { - BlocProvider.of( - context, - ).setDefaultRemote(remote.identifier); - }, - ), - ), - ); - }, + ); + }, + wrapBuilder: false, + fillRemaining: true, + ), + }, +); + +List buildConnectionsSettingsActions( + BuildContext context, + ButterflySettings state, +) => [ + IconButton( + icon: const PhosphorIcon(PhosphorIconsLight.sealQuestion), + tooltip: AppLocalizations.of(context).help, + onPressed: () => openHelp(['storage'], 'remote'), + ), + IconButton( + icon: state.defaultRemote.isEmpty + ? const PhosphorIcon(PhosphorIconsFill.house) + : const PhosphorIcon(PhosphorIconsLight.house), + tooltip: state.defaultRemote.isEmpty + ? AppLocalizations.of(context).defaultConnection + : AppLocalizations.of(context).notDefaultConnection, + onPressed: () => context.read().setDefaultRemote(''), + ), +]; + +Widget? buildConnectionsSettingsFloatingActionButton( + BuildContext context, + ButterflySettings state, +) { + if (kIsWeb) return null; + return FloatingActionButton.extended( + onPressed: () => + showLeapBottomSheet( + context: context, + titleBuilder: (context) => + Text(AppLocalizations.of(context).addConnection), + childrenBuilder: (context) => getSupportedStorages() + .map( + (e) => ListTile( + title: Text(e.getLocalizedTypeName(context)), + leading: PhosphorIcon(e.typeIcon(PhosphorIconsStyle.light)), + onTap: () => Navigator.pop(context, e), ), - ); - }, + ) + .toList(), + ).then((value) { + if (value == null) return; + showDialog( + context: context, + builder: (context) => _AddRemoteDialog(storage: value), ); - }, - ), - ); - } + }), + label: Text(AppLocalizations.of(context).addConnection), + icon: const PhosphorIcon(PhosphorIconsLight.plus), + ); } String _formatSha1Uint8List(Uint8List sha1Bytes) { diff --git a/app/lib/settings/pages/data.dart b/app/lib/settings/pages/data.dart new file mode 100644 index 000000000000..82356391e122 --- /dev/null +++ b/app/lib/settings/pages/data.dart @@ -0,0 +1,60 @@ +part of '../home.dart'; + +final _dataSettingsPage = SettingsLeapPage( + id: 'data', + displayName: (context) => AppLocalizations.of(context).data, + icon: PhosphorIconsLight.database, + appBarBuilder: _butterflyAppBar, + sections: { + 'storage': SettingsLeapSection( + settings: [ + SettingsLeapEnumSetting( + displayName: (context) => AppLocalizations.of(context).syncMode, + icon: PhosphorIconsLight.cloudArrowDown, + enabled: (context, state) => !kIsWeb, + values: SyncMode.values, + read: (state) => state.syncMode, + write: (context, value) => + context.read().changeSyncMode(value), + valueLabel: (context, value) => value.getLocalizedName(context), + ), + SettingsLeapCustomSetting( + displayName: (context) => AppLocalizations.of(context).dataDirectory, + enabled: (context, state) => !kIsWeb, + builder: buildDataDirectorySetting, + ), + SettingsLeapActionSetting( + displayName: (context) => AppLocalizations.of(context).templates, + icon: PhosphorIconsLight.file, + onTap: openTemplatesDialog, + ), + SettingsLeapActionSetting( + displayName: (context) => AppLocalizations.of(context).packs, + icon: PhosphorIconsLight.package, + onTap: openPacksDialog, + ), + SettingsLeapActionSetting( + displayName: (context) => AppLocalizations.of(context).exportAllFiles, + icon: PhosphorIconsLight.export, + onTap: exportData, + ), + ], + ), + 'settings': SettingsLeapSection( + settings: [ + SettingsLeapActionSetting( + displayName: (context) => + AppLocalizations.of(context).restoreSettingsFromFile, + icon: PhosphorIconsLight.arrowSquareIn, + onTap: importSettings, + ), + SettingsLeapActionSetting( + displayName: (context) => + AppLocalizations.of(context).exportSettingsToFile, + icon: PhosphorIconsLight.arrowSquareOut, + onTap: exportSettings, + ), + ], + ), + }, +); diff --git a/app/lib/settings/pages/experiments.dart b/app/lib/settings/pages/experiments.dart new file mode 100644 index 000000000000..ab2e7ce4e2f4 --- /dev/null +++ b/app/lib/settings/pages/experiments.dart @@ -0,0 +1,66 @@ +part of '../home.dart'; + +final _experimentsSettingsPage = SettingsLeapPage( + id: 'experiments', + displayName: (context) => AppLocalizations.of(context).experiments, + icon: PhosphorIconsLight.flask, + keywordsBuilder: (context) => [ + AppLocalizations.of(context).collaboration, + AppLocalizations.of(context).smoothNavigation, + AppLocalizations.of(context).edgePanAreaSwitching, + ], + appBarBuilder: _butterflyAppBar, + actionsBuilder: _experimentsActions, + sections: { + 'flags': SettingsLeapSection( + headerBuilder: _experimentsHeader, + settings: [ + SettingsLeapBoolSetting( + displayName: (context) => AppLocalizations.of(context).collaboration, + icon: PhosphorIconsLight.chatTeardrop, + read: (state) => state.hasFlag('collaboration'), + write: (context, value) => + _changeFlag(context, 'collaboration', value), + ), + SettingsLeapBoolSetting( + displayName: (context) => + AppLocalizations.of(context).smoothNavigation, + icon: PhosphorIconsLight.caretCircleDoubleDown, + read: (state) => state.hasFlag('smoothNavigation'), + write: (context, value) => + _changeFlag(context, 'smoothNavigation', value), + ), + SettingsLeapBoolSetting( + displayName: (context) => + AppLocalizations.of(context).edgePanAreaSwitching, + icon: PhosphorIconsLight.cursor, + read: (state) => state.hasFlag('edgePanAreaSwitching'), + write: (context, value) => + _changeFlag(context, 'edgePanAreaSwitching', value), + ), + ], + ), + }, +); + +List _experimentsActions( + BuildContext context, + ButterflySettings state, +) => [ + IconButton( + icon: const PhosphorIcon(PhosphorIconsLight.clockCounterClockwise), + tooltip: LeapLocalizations.of(context).reset, + onPressed: () => context.read().resetFlags(), + ), +]; + +Widget _experimentsHeader(BuildContext context, ButterflySettings state) { + return Row( + mainAxisSize: MainAxisSize.min, + spacing: 16, + children: [ + const Icon(PhosphorIconsLight.warning), + Flexible(child: Text(AppLocalizations.of(context).experimentsWarning)), + ], + ); +} diff --git a/app/lib/settings/pages/general.dart b/app/lib/settings/pages/general.dart new file mode 100644 index 000000000000..c9f067e8320f --- /dev/null +++ b/app/lib/settings/pages/general.dart @@ -0,0 +1,261 @@ +part of '../home.dart'; + +final _generalSettingsPage = SettingsLeapPage( + id: 'general', + displayName: (context) => AppLocalizations.of(context).general, + icon: PhosphorIconsLight.gear, + appBarBuilder: _butterflyAppBar, + sections: { + 'update': SettingsLeapSection( + displayName: (context) => AppLocalizations.of(context).update, + settings: [ + SettingsLeapCustomSetting( + displayName: (context) => AppLocalizations.of(context).currentVersion, + builder: _currentVersionSetting, + ), + SettingsLeapCustomSetting( + displayName: (context) => + AppLocalizations.of(context).checkForUpdates, + enabled: (context, state) => !kIsWeb, + builder: _updateCheckSetting, + ), + ], + ), + 'community': SettingsLeapSection( + settings: [ + SettingsLeapActionSetting( + displayName: (context) => AppLocalizations.of(context).documentation, + icon: PhosphorIconsLight.article, + onTap: _openDocumentation, + ), + SettingsLeapActionSetting( + displayName: (context) => AppLocalizations.of(context).releaseNotes, + icon: PhosphorIconsLight.flag, + onTap: (context) => openReleaseNotes(), + ), + SettingsLeapActionSetting( + displayName: (context) => 'Matrix', + icon: PhosphorIconsLight.users, + onTap: _openMatrix, + ), + SettingsLeapActionSetting( + displayName: (context) => 'Discord', + icon: PhosphorIconsLight.users, + onTap: _openDiscord, + ), + SettingsLeapActionSetting( + displayName: (context) => AppLocalizations.of(context).translate, + icon: PhosphorIconsLight.translate, + onTap: _openTranslate, + ), + SettingsLeapActionSetting( + displayName: (context) => AppLocalizations.of(context).sourceCode, + icon: PhosphorIconsLight.code, + onTap: _openSourceCode, + ), + SettingsLeapActionSetting( + displayName: (context) => AppLocalizations.of(context).changelog, + icon: PhosphorIconsLight.arrowCounterClockwise, + onTap: _openChangelog, + ), + ], + ), + 'legal': SettingsLeapSection( + settings: [ + SettingsLeapActionSetting( + displayName: (context) => AppLocalizations.of(context).license, + icon: PhosphorIconsLight.stack, + onTap: _openLicense, + ), + SettingsLeapActionSetting( + displayName: (context) => AppLocalizations.of(context).imprint, + icon: PhosphorIconsLight.identificationCard, + onTap: _openImprint, + ), + SettingsLeapActionSetting( + displayName: (context) => AppLocalizations.of(context).privacypolicy, + icon: PhosphorIconsLight.shield, + onTap: _openPrivacyPolicy, + ), + SettingsLeapActionSetting( + displayName: (context) => + AppLocalizations.of(context).thirdPartyLicenses, + icon: PhosphorIconsLight.file, + onTap: (context) => showLicensePage(context: context), + ), + ], + ), + }, +); + +class _Meta { + const _Meta({ + required this.stableVersion, + required this.nightlyVersion, + required this.developVersion, + required this.mainVersion, + }); + + _Meta.fromJson(Map json) + : stableVersion = json['version']?['stable'] ?? '?', + nightlyVersion = json['version']?['nightly'] ?? '?', + developVersion = json['version']?['develop'] ?? '?', + mainVersion = json['version']?['main'] ?? '?'; + + final String stableVersion; + final String nightlyVersion; + final String developVersion; + final String mainVersion; +} + +Widget _currentVersionSetting(BuildContext context, ButterflySettings state) { + return FutureBuilder( + future: getCurrentVersion(), + builder: (context, snapshot) { + final currentVersion = snapshot.data ?? '?'; + final currentVersionName = '$applicationVersionName $currentVersion'; + return ListTile( + title: Text(AppLocalizations.of(context).currentVersion), + subtitle: Text(currentVersionName), + onTap: () => saveToClipboard(context, currentVersion), + ); + }, + ); +} + +class _UpdateCheckSetting extends StatefulWidget { + const _UpdateCheckSetting(); + + @override + State<_UpdateCheckSetting> createState() => _UpdateCheckSettingState(); +} + +class _UpdateCheckSettingState extends State<_UpdateCheckSetting> { + Future<_Meta>? _metaFuture; + final Future _currentVersion = getCurrentVersion(); + + void _loadMeta() => setState(() { + _metaFuture = _fetchMeta(); + }); + + Future<_Meta> _fetchMeta() async { + final response = await http.get( + Uri.parse('https://butterfly.linwood.dev/meta.json'), + ); + return _Meta.fromJson({...json.decode(response.body)}); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _currentVersion, + builder: (context, snapshot) { + final currentVersion = snapshot.data ?? '?'; + return FutureBuilder<_Meta>( + future: _metaFuture, + builder: (context, snapshot) { + if (snapshot.hasError) { + return ListTile( + title: Text(AppLocalizations.of(context).error), + subtitle: Text('${snapshot.error}'), + ); + } + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + if (!snapshot.hasData) { + return ListTile( + title: Text(AppLocalizations.of(context).checkForUpdates), + subtitle: Text( + AppLocalizations.of(context).checkForUpdatesWarning, + ), + onTap: _loadMeta, + ); + } + final meta = snapshot.data!; + final stableVersion = meta.stableVersion; + final nightlyVersion = meta.nightlyVersion; + final isStable = currentVersion == stableVersion; + final isNightly = currentVersion == nightlyVersion; + final isDevelop = currentVersion == meta.developVersion; + final isMain = currentVersion == meta.mainVersion; + final isError = + meta.nightlyVersion == '?' || meta.stableVersion == '?'; + final isUpdateAvailable = + !isError && !isStable && !isNightly && !isDevelop && !isMain; + return Column( + children: [ + ListTile( + title: Text(AppLocalizations.of(context).stable), + subtitle: Text(stableVersion), + onTap: () => saveToClipboard(context, stableVersion), + ), + ListTile( + title: Text(AppLocalizations.of(context).nightly), + subtitle: Text(nightlyVersion), + onTap: () => saveToClipboard(context, nightlyVersion), + ), + const Divider(), + if (isStable) + ListTile( + title: Text(AppLocalizations.of(context).usingLatestStable), + ) + else if (isNightly || isDevelop || isMain) + ListTile( + title: Text( + AppLocalizations.of(context).usingLatestNightly, + ), + ) + else if (isError) + ListTile(title: Text(AppLocalizations.of(context).error)) + else if (isUpdateAvailable) + ListTile( + title: Text(AppLocalizations.of(context).updateAvailable), + subtitle: Text(AppLocalizations.of(context).updateNow), + leading: const PhosphorIcon(PhosphorIconsLight.arrowRight), + onTap: _openDownloads, + ), + ], + ); + }, + ); + }, + ); + } +} + +Widget _updateCheckSetting(BuildContext context, ButterflySettings state) => + const _UpdateCheckSetting(); + +Future _openUrl(Uri uri) => + launchUrl(uri, mode: LaunchMode.externalApplication); + +void _openDocumentation(BuildContext context) => + _openUrl(Uri.https('butterfly.linwood.dev', '')); + +void _openMatrix(BuildContext context) => + _openUrl(Uri.https('go.linwood.dev', 'matrix')); + +void _openDiscord(BuildContext context) => + _openUrl(Uri.https('go.linwood.dev', 'discord')); + +void _openTranslate(BuildContext context) => + _openUrl(Uri.https('go.linwood.dev', 'butterfly/translate')); + +void _openSourceCode(BuildContext context) => + _openUrl(Uri.https('go.linwood.dev', 'butterfly/source')); + +void _openChangelog(BuildContext context) => + _openUrl(Uri.https('butterfly.linwood.dev', 'changelog')); + +void _openLicense(BuildContext context) => + _openUrl(Uri.https('go.linwood.dev', 'butterfly/license')); + +void _openImprint(BuildContext context) => + _openUrl(Uri.https('go.linwood.dev', 'imprint')); + +void _openPrivacyPolicy(BuildContext context) => + _openUrl(Uri.https('butterfly.linwood.dev', 'privacypolicy')); + +void _openDownloads() => + _openUrl(Uri.parse('https://butterfly.linwood.dev/downloads')); diff --git a/app/lib/settings/pages/inputs.dart b/app/lib/settings/pages/inputs.dart new file mode 100644 index 000000000000..1706076ceecc --- /dev/null +++ b/app/lib/settings/pages/inputs.dart @@ -0,0 +1,288 @@ +part of '../home.dart'; + +final _inputsSettingsPage = SettingsLeapPage( + id: 'inputs', + displayName: (context) => AppLocalizations.of(context).inputs, + icon: PhosphorIconsLight.keyboard, + appBarBuilder: _butterflyAppBar, + children: { + 'mouse': _mouseSettingsPage, + 'touch': _touchSettingsPage, + 'keyboard': _keyboardSettingsPage, + 'pen': _penSettingsPage, + }, + sections: { + 'devices': SettingsLeapSection( + settings: [ + SettingsLeapActionSetting( + displayName: (context) => AppLocalizations.of(context).mouse, + icon: PhosphorIconsLight.mouse, + onTap: _openMouseSettings, + ), + SettingsLeapActionSetting( + displayName: (context) => AppLocalizations.of(context).touch, + icon: PhosphorIconsLight.hand, + onTap: _openTouchSettings, + ), + SettingsLeapActionSetting( + displayName: (context) => AppLocalizations.of(context).keyboard, + icon: PhosphorIconsLight.keyboard, + onTap: _openKeyboardSettings, + ), + SettingsLeapActionSetting( + displayName: (context) => AppLocalizations.of(context).pen, + icon: PhosphorIconsLight.pen, + onTap: _openPenSettings, + ), + ], + ), + 'sensitivity': SettingsLeapSection( + displayName: (context) => AppLocalizations.of(context).sensitivity, + descriptionBuilder: (context) => + AppLocalizations.of(context).sensitivityHint, + settings: [ + SettingsLeapCustomSetting( + displayName: (context) => AppLocalizations.of(context).select, + builder: _selectSensitivitySetting, + ), + SettingsLeapCustomSetting( + displayName: (context) => AppLocalizations.of(context).touch, + builder: _touchSensitivitySetting, + ), + SettingsLeapCustomSetting( + displayName: (context) => AppLocalizations.of(context).inputGestures, + builder: _gestureSensitivitySetting, + ), + SettingsLeapCustomSetting( + displayName: (context) => AppLocalizations.of(context).scroll, + builder: _scrollSensitivitySetting, + ), + ], + ), + 'pointerTest': SettingsLeapSection( + displayName: (context) => AppLocalizations.of(context).pointerTest, + builder: _pointerTestSection, + ), + }, +); +final _mouseSettingsPage = SettingsLeapPage( + id: 'mouse', + displayName: (context) => AppLocalizations.of(context).mouse, + icon: PhosphorIconsLight.mouse, + appBarBuilder: _butterflyAppBar, + sections: { + 'content': SettingsLeapSection( + wrapBuilder: false, + builder: (context, state, inView) => MouseInputSettings(state: state), + ), + }, +); +final _touchSettingsPage = SettingsLeapPage( + id: 'touch', + displayName: (context) => AppLocalizations.of(context).touch, + icon: PhosphorIconsLight.hand, + appBarBuilder: _butterflyAppBar, + sections: { + 'content': SettingsLeapSection( + wrapBuilder: false, + builder: (context, state, inView) => TouchInputSettings(state: state), + ), + }, +); +final _keyboardSettingsPage = SettingsLeapPage( + id: 'keyboard', + displayName: (context) => AppLocalizations.of(context).keyboard, + icon: PhosphorIconsLight.keyboard, + appBarBuilder: _butterflyAppBar, + sections: { + 'content': SettingsLeapSection( + wrapBuilder: false, + builder: (context, state, inView) => KeyboardInputSettings(state: state), + ), + }, +); +final _penSettingsPage = SettingsLeapPage( + id: 'pen', + displayName: (context) => AppLocalizations.of(context).pen, + icon: PhosphorIconsLight.pen, + appBarBuilder: _butterflyAppBar, + sections: { + 'content': SettingsLeapSection( + wrapBuilder: false, + builder: (context, state, inView) => PenInputSettings(state: state), + ), + }, +); + +void _openMouseSettings(BuildContext context) => + context.push('/settings/inputs/mouse'); + +void _openTouchSettings(BuildContext context) => + context.push('/settings/inputs/touch'); + +void _openKeyboardSettings(BuildContext context) => + context.push('/settings/inputs/keyboard'); + +void _openPenSettings(BuildContext context) => + context.push('/settings/inputs/pen'); + +Widget _selectSensitivitySetting( + BuildContext context, + ButterflySettings state, +) { + return _sensitivitySlider( + context, + label: AppLocalizations.of(context).select, + value: state.selectSensitivity, + onChangeEnd: (value) => + context.read().changeSelectSensitivity(value), + ); +} + +Widget _touchSensitivitySetting(BuildContext context, ButterflySettings state) { + return _sensitivitySlider( + context, + label: AppLocalizations.of(context).touch, + value: state.touchSensitivity, + onChangeEnd: (value) => + context.read().changeTouchSensitivity(value), + ); +} + +Widget _gestureSensitivitySetting( + BuildContext context, + ButterflySettings state, +) { + return _sensitivitySlider( + context, + label: AppLocalizations.of(context).inputGestures, + value: state.gestureSensitivity, + onChangeEnd: (value) => + context.read().changeGestureSensitivity(value), + ); +} + +Widget _scrollSensitivitySetting( + BuildContext context, + ButterflySettings state, +) { + return _sensitivitySlider( + context, + label: AppLocalizations.of(context).scroll, + value: state.scrollSensitivity, + onChangeEnd: (value) => + context.read().changeScrollSensitivity(value), + ); +} + +Widget _sensitivitySlider( + BuildContext context, { + required String label, + required double value, + required ValueChanged onChangeEnd, +}) { + return ExactSlider( + min: 10, + max: 1000, + defaultValue: 100, + fractionDigits: 0, + value: value * 100, + header: Text(label), + onChangeEnd: (value) => onChangeEnd(value / 100), + ); +} + +Widget _pointerTestSection( + BuildContext context, + ButterflySettings state, + Widget child, +) => const _PointerTest(); + +void _changeFlag(BuildContext context, String flag, bool enabled) { + final cubit = context.read(); + if (enabled) { + cubit.addFlag(flag); + } else { + cubit.removeFlag(flag); + } +} + +class _PointerTest extends StatefulWidget { + const _PointerTest(); + + @override + State<_PointerTest> createState() => __PointerTestState(); +} + +class __PointerTestState extends State<_PointerTest> { + PointerDeviceKind? _kind; + int _buttons = 0; + double? _pressure, _pressureMin, _pressureMax; + Color? _pressed; + + void Function(PointerEvent event) _changeInputTest(Color? color) => + (PointerEvent event) { + setState(() { + _kind = event.kind; + _buttons = event.buttons; + _pressure = event.pressure; + _pressureMin = event.pressureMin; + _pressureMax = event.pressureMax; + _pressed = color; + }); + }; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: settingsCardTitlePadding, + child: Text( + AppLocalizations.of(context).pointerTest, + style: TextTheme.of(context).headlineSmall, + ), + ), + const SizedBox(height: 16), + SizedBox( + height: 150, + child: Listener( + onPointerMove: _changeInputTest(Colors.blue), + onPointerDown: _changeInputTest(Colors.green), + onPointerUp: _changeInputTest(null), + onPointerCancel: _changeInputTest(Colors.red), + onPointerPanZoomStart: _changeInputTest(Colors.purple), + onPointerPanZoomUpdate: _changeInputTest(Colors.purple[700]), + onPointerPanZoomEnd: _changeInputTest(Colors.purple[900]), + child: Material(color: _pressed), + ), + ), + const SizedBox(height: 8), + ListTile( + title: Text(AppLocalizations.of(context).type), + subtitle: Text(switch (_kind) { + PointerDeviceKind.touch => AppLocalizations.of(context).touch, + PointerDeviceKind.mouse => AppLocalizations.of(context).mouse, + PointerDeviceKind.stylus => AppLocalizations.of(context).pen, + PointerDeviceKind.invertedStylus => AppLocalizations.of( + context, + ).invert, + PointerDeviceKind.unknown => AppLocalizations.of(context).error, + _ => AppLocalizations.of(context).none, + }), + ), + ListTile( + title: Text(AppLocalizations.of(context).input), + subtitle: Text('$_buttons (${_buttons.toRadixString(2)})'), + ), + ListTile( + title: Text(AppLocalizations.of(context).pressure), + subtitle: Text( + '${_pressure ?? '?'} (${_pressureMin ?? '?'} - ${_pressureMax ?? '?'})', + ), + ), + ], + ); + } +} diff --git a/app/lib/settings/pages/logs.dart b/app/lib/settings/pages/logs.dart new file mode 100644 index 000000000000..07b261708e40 --- /dev/null +++ b/app/lib/settings/pages/logs.dart @@ -0,0 +1,277 @@ +part of '../home.dart'; + +final _logsSettingsPage = SettingsLeapPage( + id: 'logs', + displayName: (context) => AppLocalizations.of(context).logs, + icon: PhosphorIconsLight.bug, + appBarBuilder: _butterflyAppBar, + sections: { + 'content': SettingsLeapSection( + settings: [ + SettingsLeapBoolSetting( + displayName: (context) => AppLocalizations.of(context).logs, + read: (state) => state.showVerboseLogs, + write: (context, value) => + context.read().changeShowVerboseLogs(value), + ), + ], + builder: _logsSection, + wrapBuilder: false, + fillRemaining: true, + ), + }, +); + +Widget _logsSection( + BuildContext context, + ButterflySettings state, + Widget child, +) => const LogsSettingsContent(); + +class LogsSettingsContent extends StatefulWidget { + const LogsSettingsContent({super.key}); + + @override + State createState() => _LogsSettingsContentState(); +} + +class _LogsSettingsContentState extends State { + List _archivedFiles = []; + File? _selectedFile; + List _selectedArchivedLogs = []; + + @override + void initState() { + super.initState(); + _loadArchiveList(); + } + + Future _loadArchiveList() async { + final archives = await getArchivedLogs(); + setState(() { + _archivedFiles = archives; + }); + } + + Future _selectArchive(File? file) async { + if (file == null) { + setState(() { + _selectedFile = null; + _selectedArchivedLogs = []; + }); + return; + } + final logs = await loadArchivedLogFile(file); + setState(() { + _selectedFile = file; + _selectedArchivedLogs = logs; + }); + } + + String _getFileName(File file) { + try { + final name = file.path.split(Platform.pathSeparator).last; + return name.replaceAll('logs_', '').replaceAll('.json', ''); + } catch (_) { + return 'Archive'; + } + } + + Widget _buildList(List allLogs, bool showVerbose) { + final logs = allLogs.where((element) { + if (showVerbose) return true; + return element.logLevel != LogLevel.verbose && + element.logLevel != LogLevel.debug; + }).toList(); + if (logs.isEmpty) { + return const Center(child: Text('No logs')); + } + return ListView.builder( + itemCount: logs.length, + itemBuilder: (context, index) { + final log = logs[logs.length - 1 - index]; + return _LogTile(log: log); + }, + ); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + talker.configure( + settings: talker.settings.copyWith( + useConsoleLogs: state.showVerboseLogs, + ), + ); + return Column( + children: [ + if (_archivedFiles.isNotEmpty) + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: DropdownMenuFormField( + initialSelection: _selectedFile, + expandedInsets: EdgeInsets.zero, + dropdownMenuEntries: [ + DropdownMenuEntry( + value: null, + label: AppLocalizations.of(context).currentVersion, + ), + ..._archivedFiles.map( + (file) => DropdownMenuEntry( + value: file, + label: _getFileName(file), + ), + ), + ], + onSelected: _selectArchive, + ), + ), + ), + + IconButton( + icon: const PhosphorIcon(PhosphorIconsLight.copy), + tooltip: AppLocalizations.of(context).copy, + onPressed: () { + final sourceLogs = _selectedFile == null + ? talker.history + : _selectedArchivedLogs; + final text = sourceLogs + .map((e) => e.generateTextMessage()) + .join('\n'); + Clipboard.setData(ClipboardData(text: text)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context).copyTitle), + ), + ); + }, + ), + IconButton( + icon: const PhosphorIcon(PhosphorIconsLight.trash), + tooltip: AppLocalizations.of(context).delete, + onPressed: () { + talker.cleanHistory(); + clearPersistedLogs(); + _loadArchiveList(); + _selectArchive(null); + setState(() {}); + }, + ), + ], + ), + SwitchListTile( + title: const Text('Show verbose logs'), + value: state.showVerboseLogs, + onChanged: (value) { + context.read().changeShowVerboseLogs(value); + }, + ), + Expanded( + child: _selectedFile == null + ? StreamBuilder( + stream: talker.stream, + builder: (context, snapshot) { + return _buildList( + talker.history, + state.showVerboseLogs, + ); + }, + ) + : _buildList(_selectedArchivedLogs, state.showVerboseLogs), + ), + ], + ); + }, + ); + } +} + +class _LogTile extends StatelessWidget { + final TalkerData log; + const _LogTile({required this.log}); + + @override + Widget build(BuildContext context) { + Color? color; + switch (log.logLevel) { + case LogLevel.error: + case LogLevel.critical: + color = Colors.red; + break; + case LogLevel.warning: + color = Colors.orange; + break; + case LogLevel.verbose: + case LogLevel.debug: + color = Colors.grey; + break; + case LogLevel.info: + default: + break; + } + + return ListTile( + title: Text( + log.generateTextMessage(), + style: TextStyle(color: color), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text(DateFormat('HH:mm:ss').format(log.time)), + trailing: IconButton( + icon: const PhosphorIcon(PhosphorIconsLight.copy), + onPressed: () { + Clipboard.setData(ClipboardData(text: log.generateTextMessage())); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(AppLocalizations.of(context).copyTitle)), + ); + }, + ), + onTap: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(log.title ?? 'Log'), + content: SingleChildScrollView( + child: SelectableText(log.generateTextMessage()), + ), + actions: [ + TextButton( + onPressed: () { + Clipboard.setData( + ClipboardData(text: log.generateTextMessage()), + ); + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context).copyTitle), + ), + ); + }, + child: Text(AppLocalizations.of(context).copy), + ), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ), + ); + }, + ); + } +} + +String _startupBehaviorName(BuildContext context, StartupBehavior value) => + switch (value) { + StartupBehavior.openHomeScreen => AppLocalizations.of(context).homeScreen, + StartupBehavior.openLastNote => AppLocalizations.of(context).lastNote, + StartupBehavior.openNewNote => AppLocalizations.of(context).newNote, + }; diff --git a/app/lib/settings/pages/personalization.dart b/app/lib/settings/pages/personalization.dart new file mode 100644 index 000000000000..c3d402356a05 --- /dev/null +++ b/app/lib/settings/pages/personalization.dart @@ -0,0 +1,177 @@ +part of '../home.dart'; + +final _personalizationSettingsPage = SettingsLeapPage( + id: 'personalization', + displayName: (context) => AppLocalizations.of(context).personalization, + icon: PhosphorIconsLight.monitor, + appBarBuilder: _butterflyAppBar, + sections: { + 'content': SettingsLeapSection( + settings: [ + SettingsLeapEnumSetting( + displayName: (context) => AppLocalizations.of(context).theme, + icon: PhosphorIconsLight.eye, + values: ThemeMode.values, + read: (state) => state.theme, + write: (context, value) => + context.read().changeTheme(value, context), + valueLabel: _themeName, + ), + SettingsLeapCustomSetting( + displayName: (context) => AppLocalizations.of(context).design, + builder: _designSetting, + ), + SettingsLeapCustomSetting( + displayName: (context) => AppLocalizations.of(context).locale, + builder: _localeSetting, + ), + SettingsLeapEnumSetting( + displayName: (context) => AppLocalizations.of(context).platformTheme, + icon: PhosphorIconsLight.cursor, + values: PlatformTheme.values, + read: (state) => state.platformTheme, + write: (context, value) => + context.read().changePlatformTheme(value), + valueLabel: _platformThemeName, + ), + SettingsLeapEnumSetting( + displayName: (context) => AppLocalizations.of(context).density, + icon: PhosphorIconsLight.gridNine, + values: ThemeDensity.values, + read: (state) => state.density, + write: (context, value) => + context.read().changeDensity(value), + valueLabel: _densityName, + ), + SettingsLeapBoolSetting( + displayName: (context) => AppLocalizations.of(context).highContrast, + icon: PhosphorIconsLight.circleHalf, + read: (state) => state.highContrast, + write: (context, value) => + context.read().changeHighContrast(value), + ), + SettingsLeapBoolSetting( + displayName: (context) => AppLocalizations.of(context).nativeTitleBar, + icon: PhosphorIconsLight.appWindow, + enabled: (context, state) => !kIsWeb && isWindow, + read: (state) => state.nativeTitleBar, + write: (context, value) => + context.read().changeNativeTitleBar(value), + ), + ], + ), + }, +); + +String _themeName(BuildContext context, ThemeMode mode) => switch (mode) { + ThemeMode.system => AppLocalizations.of(context).systemTheme, + ThemeMode.light => AppLocalizations.of(context).lightTheme, + ThemeMode.dark => AppLocalizations.of(context).darkTheme, +}; + +String _platformThemeName(BuildContext context, PlatformTheme theme) => + switch (theme) { + PlatformTheme.system => AppLocalizations.of(context).systemTheme, + PlatformTheme.desktop => AppLocalizations.of(context).desktop, + PlatformTheme.mobile => AppLocalizations.of(context).mobile, + }; + +String _localeName(BuildContext context, String locale) => locale.isNotEmpty + ? LocaleNames.of(context)?.nameOf(locale.replaceAll('-', '_')) ?? locale + : AppLocalizations.of(context).systemLocale; + +String _densityName(BuildContext context, ThemeDensity density) => + switch (density) { + ThemeDensity.system => AppLocalizations.of(context).systemTheme, + ThemeDensity.maximize => AppLocalizations.of(context).densityMaximize, + ThemeDensity.desktop => AppLocalizations.of(context).desktop, + ThemeDensity.compact => AppLocalizations.of(context).compact, + ThemeDensity.standard => AppLocalizations.of(context).standard, + ThemeDensity.comfortable => AppLocalizations.of(context).comfortable, + }; + +Widget _designSetting(BuildContext context, ButterflySettings state) { + final design = state.design; + return ListTile( + leading: const PhosphorIcon(PhosphorIconsLight.palette), + title: Text(AppLocalizations.of(context).design), + subtitle: Text( + design.isEmpty ? AppLocalizations.of(context).systemTheme : design, + ), + trailing: ThemeBox(theme: getThemeData(state.design, false)), + onTap: () => _openDesignModal(context), + ); +} + +Widget _localeSetting(BuildContext context, ButterflySettings state) { + return ListTile( + leading: const PhosphorIcon(PhosphorIconsLight.translate), + title: Text(AppLocalizations.of(context).locale), + subtitle: Text(_localeName(context, state.localeTag)), + onTap: () => _openLocaleModal(context), + ); +} + +void _openDesignModal(BuildContext context) { + final cubit = context.read(); + final design = cubit.state.design; + showLeapBottomSheet( + context: context, + titleBuilder: (context) => Text(AppLocalizations.of(context).design), + childrenBuilder: (context) { + void changeDesign(String design) { + cubit.changeDesign(design); + Navigator.of(context).pop(); + } + + return [ + ListTile( + title: Text(AppLocalizations.of(context).systemTheme), + selected: design.isEmpty, + onTap: () => changeDesign(''), + leading: ThemeBox(theme: getThemeData('', false)), + ), + ...getThemes().map((e) { + final theme = getThemeData(e, false); + return ListTile( + title: Text(e), + selected: e == design, + onTap: () => changeDesign(e), + leading: ThemeBox(theme: theme), + ); + }), + ]; + }, + ); +} + +void _openLocaleModal(BuildContext context) { + final cubit = context.read(); + final currentLocale = cubit.state.localeTag; + final locales = getLocales(); + showLeapBottomSheet( + context: context, + titleBuilder: (context) => Text(AppLocalizations.of(context).locale), + childrenBuilder: (context) { + void changeLocale(Locale? locale) { + cubit.changeLocale(locale); + Navigator.of(context).pop(); + } + + return [ + ListTile( + title: Text(AppLocalizations.of(context).systemLocale), + selected: currentLocale.isEmpty, + onTap: () => changeLocale(null), + ), + ...locales.map( + (e) => ListTile( + title: Text(_localeName(context, e.toLanguageTag())), + selected: currentLocale == e.toLanguageTag(), + onTap: () => changeLocale(e), + ), + ), + ]; + }, + ); +} diff --git a/app/lib/settings/pages/view.dart b/app/lib/settings/pages/view.dart new file mode 100644 index 000000000000..eb9bc7de3b1e --- /dev/null +++ b/app/lib/settings/pages/view.dart @@ -0,0 +1,310 @@ +part of '../home.dart'; + +final _viewSettingsPage = SettingsLeapPage( + id: 'view', + displayName: (context) => AppLocalizations.of(context).view, + icon: PhosphorIconsLight.eye, + appBarBuilder: _butterflyAppBar, + sections: { + 'interface': SettingsLeapSection( + settings: [ + SettingsLeapCustomSetting( + displayName: (context) => AppLocalizations.of(context).zoomControl, + builder: _zoomControlSetting, + ), + SettingsLeapEnumSetting( + displayName: (context) => AppLocalizations.of(context).properties, + icon: PhosphorIconsLight.sliders, + values: ZoomPosition.values, + read: (state) => state.propertyPosition, + write: (context, value) => + context.read().changePropertyPosition(value), + valueLabel: (context, value) => value.getLocalizedName(context), + ), + SettingsLeapEnumSetting( + displayName: (context) => + AppLocalizations.of(context).toolbarPosition, + icon: PhosphorIconsLight.toolbox, + values: ToolbarPosition.values, + read: (state) => state.toolbarPosition, + write: (context, value) => + context.read().changeToolbarPosition(value), + valueLabel: (context, value) => value.getLocalizedName(context), + ), + SettingsLeapEnumSetting( + displayName: (context) => AppLocalizations.of(context).toolbarSize, + icon: PhosphorIconsLight.toolbox, + values: ToolbarSize.values, + read: (state) => state.toolbarSize, + write: (context, value) => + context.read().changeToolbarSize(value), + valueLabel: (context, value) => value.getLocalizedName(context), + ), + SettingsLeapCustomSetting( + displayName: (context) => AppLocalizations.of(context).toolbarRows, + builder: _toolbarRowsSetting, + ), + SettingsLeapCustomSetting( + displayName: (context) => AppLocalizations.of(context).navigationRail, + builder: _navigationRailSetting, + ), + SettingsLeapEnumSetting( + displayName: (context) => + AppLocalizations.of(context).optionsPanelPosition, + icon: PhosphorIconsLight.archive, + values: OptionsPanelPosition.values, + read: (state) => state.optionsPanelPosition, + write: (context, value) => + context.read().changeOptionsPanelPosition(value), + valueLabel: (context, value) => value.getLocalizedName(context), + ), + SettingsLeapEnumSetting( + displayName: (context) => + AppLocalizations.of(context).simpleToolbarVisibility, + icon: PhosphorIconsLight.cursorText, + values: SimpleToolbarVisibility.values, + read: (state) => state.simpleToolbarVisibility, + write: (context, value) => context + .read() + .changeSimpleToolbarVisibility(value), + valueLabel: (context, value) => value.getLocalizedName(context), + ), + ], + ), + 'home': SettingsLeapSection( + displayName: (context) => AppLocalizations.of(context).home, + settings: [ + SettingsLeapBoolSetting( + displayName: (context) => AppLocalizations.of(context).showThumbnails, + icon: PhosphorIconsLight.image, + read: (state) => state.showThumbnails, + write: (context, value) => + context.read().changeShowThumbnails(value), + ), + SettingsLeapBoolSetting( + displayName: (context) => + AppLocalizations.of(context).hideFileExtension, + icon: PhosphorIconsLight.fileText, + read: (state) => state.hideExtension, + write: (context, value) => + context.read().changeHideExtension(value), + ), + ], + ), + }, +); + +Widget _contentViewportSetting(BuildContext context, ButterflySettings state) { + return ListTile( + leading: const PhosphorIcon(PhosphorIconsLight.appWindow), + title: Text(AppLocalizations.of(context).contentViewport), + subtitle: Text( + state.limitViewportMultiplier == null + ? AppLocalizations.of(context).off + : '${state.limitViewportMultiplier}x', + ), + onTap: () => _openContentViewportModal(context), + ); +} + +Widget _imageScaleSetting(BuildContext context, ButterflySettings state) { + return ExactSlider( + header: Text(AppLocalizations.of(context).imageScale), + leading: const PhosphorIcon(PhosphorIconsLight.frameCorners), + value: state.imageScale * 100, + min: 0, + max: 100, + defaultValue: 50, + fractionDigits: 0, + onChangeEnd: (value) => + context.read().changeImageScale(value / 100), + ); +} + +void _openPersistenceSettings(BuildContext context) => + context.push('/settings/behaviors/persistence'); + +void _openContentViewportModal(BuildContext context) { + final cubit = context.read(); + final currentMultiplier = cubit.state.limitViewportMultiplier; + showLeapBottomSheet( + context: context, + titleBuilder: (context) => + Text(AppLocalizations.of(context).contentViewport), + childrenBuilder: (context) { + final options = [ + (null, AppLocalizations.of(context).off), + (1.0, '1x'), + (1.5, '1.5x'), + (2.0, '2x'), + (3.0, '3x'), + ]; + return options + .map( + (e) => ListTile( + title: Text(e.$2), + selected: currentMultiplier == e.$1, + onTap: () { + cubit.changeLimitViewportMultiplier(e.$1); + Navigator.of(context).pop(); + }, + ), + ) + .toList(); + }, + ); +} + +void _openAutosaveModal(BuildContext context) { + final cubit = context.read(); + final autosave = cubit.state.autosave; + final showSaveButton = cubit.state.showSaveButton; + final delayed = cubit.state.delayedAutosave; + showLeapBottomSheet( + context: context, + titleBuilder: (context) => Text(AppLocalizations.of(context).autosave), + childrenBuilder: (context) { + void changeAutosave(bool? autosave, {bool delayed = false}) { + cubit.changeAutosave(autosave, delayed: delayed); + Navigator.of(context).pop(); + } + + return [ + ListTile( + title: Text(AppLocalizations.of(context).yes), + leading: const Icon(PhosphorIconsLight.check), + selected: autosave && !showSaveButton && !delayed, + onTap: () => changeAutosave(true), + ), + ListTile( + title: Text(AppLocalizations.of(context).delay), + leading: const Icon(PhosphorIconsLight.clock), + selected: autosave && delayed, + onTap: () => changeAutosave(null, delayed: true), + ), + ListTile( + title: Text(AppLocalizations.of(context).yesButShowButtons), + leading: const Icon(PhosphorIconsLight.question), + selected: autosave && showSaveButton && !delayed, + onTap: () => changeAutosave(null), + ), + ListTile( + title: Text(AppLocalizations.of(context).no), + leading: const Icon(PhosphorIconsLight.x), + selected: !autosave, + onTap: () => changeAutosave(false), + ), + ]; + }, + ); +} + +Widget _zoomControlSetting(BuildContext context, ButterflySettings state) { + return AdvancedSwitchListTile( + leading: const PhosphorIcon(PhosphorIconsLight.magnifyingGlass), + title: Text(AppLocalizations.of(context).zoomControl), + subtitle: Text(state.zoomPosition.getLocalizedName(context)), + value: state.zoomEnabled, + onChanged: (value) => + context.read().changeZoomEnabled(value), + onTap: () => _openZoomPositionModal(context), + ); +} + +Widget _toolbarRowsSetting(BuildContext context, ButterflySettings state) { + return ExactSlider( + header: Text(AppLocalizations.of(context).toolbarRows), + value: state.toolbarRows.toDouble(), + leading: const PhosphorIcon(PhosphorIconsLight.rows), + defaultValue: 1, + min: 1, + max: 4, + fractionDigits: 0, + headerWidth: 250, + divide: true, + onChangeEnd: (value) => + context.read().changeToolbarRows(value.round()), + ); +} + +Widget _navigationRailSetting(BuildContext context, ButterflySettings state) { + return AdvancedSwitchListTile( + leading: const PhosphorIcon(PhosphorIconsLight.sidebar), + title: Text(AppLocalizations.of(context).navigationRail), + height: 76, + subtitle: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + state.navigatorPosition == NavigatorPosition.left + ? AppLocalizations.of(context).left + : AppLocalizations.of(context).right, + ), + Text( + AppLocalizations.of(context).onlyAvailableLargerScreen, + style: TextTheme.of(context).labelSmall, + ), + ], + ), + onTap: () async { + final position = await showLeapBottomSheet( + context: context, + titleBuilder: (context) => Text(AppLocalizations.of(context).position), + childrenBuilder: (context) => [ + ListTile( + title: Text(AppLocalizations.of(context).left), + selected: state.navigatorPosition == NavigatorPosition.left, + leading: const PhosphorIcon( + PhosphorIconsLight.arrowLineLeft, + textDirection: TextDirection.ltr, + ), + onTap: () => Navigator.of(context).pop(NavigatorPosition.left), + ), + ListTile( + title: Text(AppLocalizations.of(context).right), + selected: state.navigatorPosition == NavigatorPosition.right, + leading: const PhosphorIcon( + PhosphorIconsLight.arrowLineRight, + textDirection: TextDirection.ltr, + ), + onTap: () => Navigator.of(context).pop(NavigatorPosition.right), + ), + ], + ); + if (position != null && context.mounted) { + context.read().changeNavigatorPosition(position); + } + }, + value: state.navigationRail, + onChanged: (value) => + context.read().changeNavigationRail(value), + ); +} + +void _openZoomPositionModal(BuildContext context) { + final cubit = context.read(); + final currentPos = cubit.state.zoomPosition; + showLeapBottomSheet( + context: context, + titleBuilder: (context) => Text(AppLocalizations.of(context).zoomPosition), + childrenBuilder: (context) => ZoomPosition.values + .map( + (e) => ListTile( + title: Text(e.getLocalizedName(context)), + selected: currentPos == e, + leading: Icon(switch (e) { + ZoomPosition.topRight => PhosphorIconsLight.arrowUpRight, + ZoomPosition.topLeft => PhosphorIconsLight.arrowUpLeft, + ZoomPosition.bottomRight => PhosphorIconsLight.arrowDownRight, + ZoomPosition.bottomLeft => PhosphorIconsLight.arrowDownLeft, + }, textDirection: TextDirection.ltr), + onTap: () { + cubit.changeZoomPosition(e); + Navigator.of(context).pop(); + }, + ), + ) + .toList(), + ); +} diff --git a/app/lib/settings/personalization.dart b/app/lib/settings/personalization.dart deleted file mode 100644 index 1ff77577d1fd..000000000000 --- a/app/lib/settings/personalization.dart +++ /dev/null @@ -1,298 +0,0 @@ -import 'package:butterfly/cubits/settings.dart'; -import 'package:butterfly/main.dart'; -import 'package:butterfly/theme.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:butterfly/src/generated/i18n/app_localizations.dart'; -import 'package:flutter_localized_locales/flutter_localized_locales.dart'; -import 'package:material_leap/material_leap.dart'; -import 'package:phosphor_flutter/phosphor_flutter.dart'; - -class PersonalizationSettingsPage extends StatelessWidget { - final bool inView; - const PersonalizationSettingsPage({super.key, this.inView = false}); - - String _getThemeName(BuildContext context, ThemeMode mode) => switch (mode) { - ThemeMode.system => AppLocalizations.of(context).systemTheme, - ThemeMode.light => AppLocalizations.of(context).lightTheme, - ThemeMode.dark => AppLocalizations.of(context).darkTheme, - }; - - String _getPlatformThemeName(BuildContext context, PlatformTheme theme) => - switch (theme) { - PlatformTheme.system => AppLocalizations.of(context).systemTheme, - PlatformTheme.desktop => AppLocalizations.of(context).desktop, - PlatformTheme.mobile => AppLocalizations.of(context).mobile, - }; - - String _getLocaleName(BuildContext context, String locale) => - locale.isNotEmpty - ? LocaleNames.of(context)?.nameOf(locale.replaceAll('-', '_')) ?? locale - : AppLocalizations.of(context).systemLocale; - - String _getDensityName(BuildContext context, ThemeDensity density) => - switch (density) { - ThemeDensity.system => AppLocalizations.of(context).systemTheme, - ThemeDensity.maximize => AppLocalizations.of(context).densityMaximize, - ThemeDensity.desktop => AppLocalizations.of(context).desktop, - ThemeDensity.compact => AppLocalizations.of(context).compact, - ThemeDensity.standard => AppLocalizations.of(context).standard, - ThemeDensity.comfortable => AppLocalizations.of(context).comfortable, - }; - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: inView ? Colors.transparent : null, - appBar: WindowTitleBar( - inView: inView, - backgroundColor: inView ? Colors.transparent : null, - title: Text(AppLocalizations.of(context).personalization), - ), - body: BlocBuilder( - builder: (context, state) { - final design = state.design; - return ListView( - children: [ - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.eye), - title: Text(AppLocalizations.of(context).theme), - subtitle: Text(_getThemeName(context, state.theme)), - onTap: () => _openThemeModal(context), - ), - ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.palette), - title: Text(AppLocalizations.of(context).design), - subtitle: Text( - design.isEmpty - ? AppLocalizations.of(context).systemTheme - : design, - ), - trailing: ThemeBox( - theme: getThemeData(state.design, false), - ), - onTap: () => _openDesignModal(context), - ), - ListTile( - leading: const PhosphorIcon( - PhosphorIconsLight.translate, - ), - title: Text(AppLocalizations.of(context).locale), - subtitle: Text( - _getLocaleName(context, state.localeTag), - ), - onTap: () => _openLocaleModal(context), - ), - ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.cursor), - title: Text(AppLocalizations.of(context).platformTheme), - subtitle: Text( - _getPlatformThemeName(context, state.platformTheme), - ), - onTap: () => _openPlatformThemeModal(context), - ), - ListTile( - leading: const PhosphorIcon( - PhosphorIconsLight.gridNine, - ), - title: Text(AppLocalizations.of(context).density), - subtitle: Text(_getDensityName(context, state.density)), - onTap: () => _openDensityModal(context), - ), - SwitchListTile( - secondary: const PhosphorIcon( - PhosphorIconsLight.circleHalf, - ), - title: Text(AppLocalizations.of(context).highContrast), - value: state.highContrast, - onChanged: (value) => context - .read() - .changeHighContrast(value), - ), - if (!kIsWeb && isWindow) - SwitchListTile( - value: state.nativeTitleBar, - title: Text( - AppLocalizations.of(context).nativeTitleBar, - ), - secondary: const PhosphorIcon( - PhosphorIconsLight.appWindow, - ), - onChanged: (value) => context - .read() - .changeNativeTitleBar(value), - ), - ], - ), - ), - ), - ], - ); - }, - ), - ); - } - - void _openDensityModal(BuildContext context) { - final cubit = context.read(); - final density = cubit.state.density; - - showLeapBottomSheet( - context: context, - titleBuilder: (context) => Text(AppLocalizations.of(context).density), - childrenBuilder: (context) { - void changeDensity(ThemeDensity density) { - cubit.changeDensity(density); - Navigator.of(context).pop(); - } - - return ThemeDensity.values - .map( - (e) => ListTile( - title: Text(_getDensityName(context, e)), - selected: e == density, - onTap: () => changeDensity(e), - ), - ) - .toList(); - }, - ); - } - - void _openDesignModal(BuildContext context) { - final cubit = context.read(); - final design = cubit.state.design; - - showLeapBottomSheet( - context: context, - titleBuilder: (context) => Text(AppLocalizations.of(context).design), - childrenBuilder: (context) { - void changeDesign(String design) { - cubit.changeDesign(design); - Navigator.of(context).pop(); - } - - return [ - ListTile( - title: Text(AppLocalizations.of(context).systemTheme), - selected: design.isEmpty, - onTap: () => changeDesign(''), - leading: ThemeBox(theme: getThemeData('', false)), - ), - ...getThemes().map((e) { - final theme = getThemeData(e, false); - return ListTile( - title: Text(e), - selected: e == design, - onTap: () => changeDesign(e), - leading: ThemeBox(theme: theme), - ); - }), - ]; - }, - ); - } - - void _openThemeModal(BuildContext context) { - final cubit = context.read(); - final currentTheme = cubit.state.theme; - - showLeapBottomSheet( - context: context, - titleBuilder: (context) => Text(AppLocalizations.of(context).theme), - childrenBuilder: (context) { - void changeTheme(ThemeMode themeMode) { - cubit.changeTheme(themeMode, context); - Navigator.of(context).pop(); - } - - return [ - ListTile( - title: Text(AppLocalizations.of(context).systemTheme), - selected: currentTheme == ThemeMode.system, - leading: const PhosphorIcon(PhosphorIconsLight.power), - onTap: () => changeTheme(ThemeMode.system), - ), - ListTile( - title: Text(AppLocalizations.of(context).lightTheme), - selected: currentTheme == ThemeMode.light, - leading: const PhosphorIcon(PhosphorIconsLight.sun), - onTap: () => changeTheme(ThemeMode.light), - ), - ListTile( - title: Text(AppLocalizations.of(context).darkTheme), - selected: currentTheme == ThemeMode.dark, - leading: const PhosphorIcon(PhosphorIconsLight.moon), - onTap: () => changeTheme(ThemeMode.dark), - ), - ]; - }, - ); - } - - void _openLocaleModal(BuildContext context) { - final cubit = context.read(); - var currentLocale = cubit.state.localeTag; - var locales = getLocales(); - showLeapBottomSheet( - context: context, - titleBuilder: (context) => Text(AppLocalizations.of(context).locale), - childrenBuilder: (context) { - void changeLocale(Locale? locale) { - cubit.changeLocale(locale); - Navigator.of(context).pop(); - } - - return [ - ListTile( - title: Text(AppLocalizations.of(context).systemLocale), - selected: currentLocale.isEmpty, - onTap: () => changeLocale(null), - ), - ...locales.map( - (e) => ListTile( - title: Text(_getLocaleName(context, e.toLanguageTag())), - selected: currentLocale == e.toLanguageTag(), - onTap: () => changeLocale(e), - ), - ), - ]; - }, - ); - } - - void _openPlatformThemeModal(BuildContext context) { - final cubit = context.read(); - var currentTheme = cubit.state.platformTheme; - showLeapBottomSheet( - context: context, - titleBuilder: (context) => - Text(AppLocalizations.of(context).platformTheme), - childrenBuilder: (context) => PlatformTheme.values - .map( - (e) => ListTile( - title: Text(_getPlatformThemeName(context, e)), - leading: PhosphorIcon(switch (e) { - PlatformTheme.system => PhosphorIconsLight.power, - PlatformTheme.desktop => PhosphorIconsLight.desktop, - PlatformTheme.mobile => PhosphorIconsLight.phone, - }), - selected: currentTheme == e, - onTap: () { - cubit.changePlatformTheme(e); - Navigator.of(context).pop(); - }, - ), - ) - .toList(), - ); - } -} diff --git a/app/lib/settings/view.dart b/app/lib/settings/view.dart deleted file mode 100644 index 29036861bd97..000000000000 --- a/app/lib/settings/view.dart +++ /dev/null @@ -1,403 +0,0 @@ -import 'package:butterfly/cubits/settings.dart'; -import 'package:butterfly/theme.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:butterfly/src/generated/i18n/app_localizations.dart'; -import 'package:material_leap/material_leap.dart'; -import 'package:phosphor_flutter/phosphor_flutter.dart'; - -class ViewSettingsPage extends StatelessWidget { - final bool inView; - - const ViewSettingsPage({super.key, this.inView = false}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: inView ? Colors.transparent : null, - appBar: WindowTitleBar( - title: Text(AppLocalizations.of(context).view), - backgroundColor: inView ? Colors.transparent : null, - inView: inView, - ), - body: BlocBuilder( - builder: (context, state) { - return ListView( - children: [ - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - AdvancedSwitchListTile( - leading: const PhosphorIcon( - PhosphorIconsLight.magnifyingGlass, - ), - title: Text(AppLocalizations.of(context).zoomControl), - subtitle: Text( - state.zoomPosition.getLocalizedName(context), - ), - value: state.zoomEnabled, - onChanged: (value) => context - .read() - .changeZoomEnabled(value), - onTap: () => _openZoomPositionModal(context), - ), - ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.sliders), - title: Text(AppLocalizations.of(context).properties), - subtitle: Text( - state.propertyPosition.getLocalizedName(context), - ), - onTap: () => _openPropertyPositionModal(context), - ), - ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.toolbox), - title: Text( - AppLocalizations.of(context).toolbarPosition, - ), - subtitle: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - state.toolbarPosition.getLocalizedName(context), - ), - Text( - AppLocalizations.of( - context, - ).onlyAvailableLargerScreen, - style: TextTheme.of(context).labelSmall, - ), - ], - ), - onTap: () => _openToolbarPositionModal(context), - ), - ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.toolbox), - title: Text(AppLocalizations.of(context).toolbarSize), - subtitle: Text( - state.toolbarSize.getLocalizedName(context), - ), - onTap: () => _openToolbarSizeModal(context), - ), - ExactSlider( - header: Text(AppLocalizations.of(context).toolbarRows), - value: state.toolbarRows.toDouble(), - leading: const PhosphorIcon(PhosphorIconsLight.rows), - defaultValue: 1, - min: 1, - max: 4, - fractionDigits: 0, - headerWidth: 250, - divide: true, - onChangeEnd: (value) => context - .read() - .changeToolbarRows(value.round()), - ), - AdvancedSwitchListTile( - leading: const PhosphorIcon(PhosphorIconsLight.sidebar), - title: Text( - AppLocalizations.of(context).navigationRail, - ), - height: 76, - subtitle: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - state.navigatorPosition == NavigatorPosition.left - ? AppLocalizations.of(context).left - : AppLocalizations.of(context).right, - ), - Text( - AppLocalizations.of( - context, - ).onlyAvailableLargerScreen, - style: TextTheme.of(context).labelSmall, - ), - ], - ), - onTap: () async { - final position = await showLeapBottomSheet( - context: context, - titleBuilder: (context) => - Text(AppLocalizations.of(context).position), - childrenBuilder: (context) => [ - ListTile( - title: Text(AppLocalizations.of(context).left), - selected: - state.navigatorPosition == - NavigatorPosition.left, - leading: const PhosphorIcon( - PhosphorIconsLight.arrowLineLeft, - textDirection: TextDirection.ltr, - ), - onTap: () => Navigator.of( - context, - ).pop(NavigatorPosition.left), - ), - ListTile( - title: Text(AppLocalizations.of(context).right), - selected: - state.navigatorPosition == - NavigatorPosition.right, - leading: const PhosphorIcon( - PhosphorIconsLight.arrowLineRight, - textDirection: TextDirection.ltr, - ), - onTap: () => Navigator.of( - context, - ).pop(NavigatorPosition.right), - ), - ], - ); - if (position != null) { - if (context.mounted) { - context - .read() - .changeNavigatorPosition(position); - } - } - }, - value: state.navigationRail, - onChanged: (value) => context - .read() - .changeNavigationRail(value), - ), - ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.archive), - title: Text( - AppLocalizations.of(context).optionsPanelPosition, - ), - subtitle: Text( - state.optionsPanelPosition.getLocalizedName(context), - ), - onTap: () => _openOptionsPanelPositionModal(context), - ), - ListTile( - leading: const PhosphorIcon( - PhosphorIconsLight.cursorText, - ), - title: Text( - AppLocalizations.of(context).simpleToolbarVisibility, - ), - subtitle: Text( - state.simpleToolbarVisibility.getLocalizedName( - context, - ), - ), - onTap: () => _openSimpleToolbarVisibilityModal(context), - ), - ], - ), - ), - ), - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: settingsCardTitlePadding, - child: Text( - AppLocalizations.of(context).home, - style: TextTheme.of(context).headlineSmall, - ), - ), - const SizedBox(height: 16), - SwitchListTile( - secondary: const PhosphorIcon(PhosphorIconsLight.image), - title: Text( - AppLocalizations.of(context).showThumbnails, - ), - value: state.showThumbnails, - onChanged: (value) => context - .read() - .changeShowThumbnails(value), - ), - SwitchListTile( - value: state.hideExtension, - onChanged: (value) => context - .read() - .changeHideExtension(value), - title: Text( - AppLocalizations.of(context).hideFileExtension, - ), - secondary: const PhosphorIcon( - PhosphorIconsLight.fileText, - ), - ), - ], - ), - ), - ), - ], - ); - }, - ), - ); - } - - void _openZoomPositionModal(BuildContext context) { - final cubit = context.read(); - var currentPos = cubit.state.zoomPosition; - showLeapBottomSheet( - context: context, - titleBuilder: (context) => - Text(AppLocalizations.of(context).zoomPosition), - childrenBuilder: (context) => ZoomPosition.values - .map( - (e) => ListTile( - title: Text(e.getLocalizedName(context)), - selected: currentPos == e, - leading: Icon(switch (e) { - ZoomPosition.topRight => PhosphorIconsLight.arrowUpRight, - ZoomPosition.topLeft => PhosphorIconsLight.arrowUpLeft, - ZoomPosition.bottomRight => PhosphorIconsLight.arrowDownRight, - ZoomPosition.bottomLeft => PhosphorIconsLight.arrowDownLeft, - }, textDirection: TextDirection.ltr), - onTap: () { - cubit.changeZoomPosition(e); - Navigator.of(context).pop(); - }, - ), - ) - .toList(), - ); - } - - void _openPropertyPositionModal(BuildContext context) { - final cubit = context.read(); - var currentPos = cubit.state.propertyPosition; - showLeapBottomSheet( - context: context, - titleBuilder: (context) => Text(AppLocalizations.of(context).properties), - childrenBuilder: (context) => ZoomPosition.values - .map( - (e) => ListTile( - title: Text(e.getLocalizedName(context)), - selected: currentPos == e, - leading: Icon(switch (e) { - ZoomPosition.topRight => PhosphorIconsLight.arrowUpRight, - ZoomPosition.topLeft => PhosphorIconsLight.arrowUpLeft, - ZoomPosition.bottomRight => PhosphorIconsLight.arrowDownRight, - ZoomPosition.bottomLeft => PhosphorIconsLight.arrowDownLeft, - }, textDirection: TextDirection.ltr), - onTap: () { - cubit.changePropertyPosition(e); - Navigator.of(context).pop(); - }, - ), - ) - .toList(), - ); - } - - void _openToolbarPositionModal(BuildContext context) { - final cubit = context.read(); - var currentPos = cubit.state.toolbarPosition; - showLeapBottomSheet( - context: context, - titleBuilder: (context) => - Text(AppLocalizations.of(context).toolbarPosition), - childrenBuilder: (context) => ToolbarPosition.values - .map( - (e) => ListTile( - title: Text(e.getLocalizedName(context)), - selected: currentPos == e, - leading: Icon(switch (e) { - ToolbarPosition.inline => PhosphorIconsLight.appWindow, - ToolbarPosition.top => PhosphorIconsLight.arrowLineUp, - ToolbarPosition.bottom => PhosphorIconsLight.arrowLineDown, - ToolbarPosition.left => PhosphorIconsLight.arrowLineLeft, - ToolbarPosition.right => PhosphorIconsLight.arrowLineRight, - }, textDirection: TextDirection.ltr), - onTap: () { - cubit.changeToolbarPosition(e); - Navigator.of(context).pop(); - }, - ), - ) - .toList(), - ); - } - - void _openOptionsPanelPositionModal(BuildContext context) { - final cubit = context.read(); - var currentPos = cubit.state.optionsPanelPosition; - showLeapBottomSheet( - context: context, - titleBuilder: (context) => - Text(AppLocalizations.of(context).optionsPanelPosition), - childrenBuilder: (context) => OptionsPanelPosition.values - .map( - (e) => ListTile( - title: Text(e.getLocalizedName(context)), - selected: currentPos == e, - leading: Icon(switch (e) { - OptionsPanelPosition.top => PhosphorIconsLight.arrowLineUp, - OptionsPanelPosition.bottom => PhosphorIconsLight.arrowLineDown, - }), - onTap: () { - cubit.changeOptionsPanelPosition(e); - Navigator.of(context).pop(); - }, - ), - ) - .toList(), - ); - } - - void _openToolbarSizeModal(BuildContext context) { - final cubit = context.read(); - var currentSize = cubit.state.toolbarSize; - showLeapBottomSheet( - context: context, - titleBuilder: (context) => Text(AppLocalizations.of(context).toolbarSize), - childrenBuilder: (context) => ToolbarSize.values - .map( - (e) => ListTile( - title: Text(e.getLocalizedName(context)), - selected: currentSize == e, - onTap: () { - cubit.changeToolbarSize(e); - Navigator.of(context).pop(); - }, - ), - ) - .toList(), - ); - } - - void _openSimpleToolbarVisibilityModal(BuildContext context) { - final cubit = context.read(); - var currentPos = cubit.state.simpleToolbarVisibility; - showLeapBottomSheet( - context: context, - titleBuilder: (context) => - Text(AppLocalizations.of(context).simpleToolbarVisibility), - childrenBuilder: (context) => SimpleToolbarVisibility.values - .map( - (e) => ListTile( - title: Text(e.getLocalizedName(context)), - selected: currentPos == e, - leading: Icon(switch (e) { - SimpleToolbarVisibility.show => PhosphorIconsLight.eye, - SimpleToolbarVisibility.hide => PhosphorIconsLight.eyeSlash, - SimpleToolbarVisibility.temporary => PhosphorIconsLight.clock, - }), - onTap: () { - cubit.changeSimpleToolbarVisibility(e); - Navigator.of(context).pop(); - }, - ), - ) - .toList(), - ); - } -} diff --git a/app/pubspec.lock b/app/pubspec.lock index 37754bdca21d..a3d4702ff3d2 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -1270,6 +1270,15 @@ packages: url: "https://pub.dev" source: hosted version: "3.8.9+1" + settings_leap: + dependency: "direct main" + description: + path: "packages/settings_leap" + ref: "81142414ae23372173056f4e56ba893fe2ff47e4" + resolved-ref: "81142414ae23372173056f4e56ba893fe2ff47e4" + url: "https://github.com/LinwoodDev/dart_pkgs.git" + source: git + version: "0.1.0" share_plus: dependency: "direct main" description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 030da982a661..f768a6e92977 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -60,6 +60,11 @@ dependencies: args: ^2.5.0 reorderable_grid: ^1.0.12 perfect_freehand: ^2.5.0 + settings_leap: + git: + url: https://github.com/LinwoodDev/dart_pkgs.git + ref: 81142414ae23372173056f4e56ba893fe2ff47e4 + path: packages/settings_leap material_leap: git: url: https://github.com/LinwoodDev/dart_pkgs.git From b4d70a2a63a2290c5be59024b7f7b98b003fafc4 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 5 Jul 2026 17:41:43 +0200 Subject: [PATCH 045/117] Improve settings code --- app/lib/settings/pages/behaviors/home.dart | 1 - .../settings/pages/behaviors/persistence.dart | 1 - app/lib/settings/pages/connections.dart | 1 - app/lib/settings/pages/data.dart | 1 - app/lib/settings/pages/experiments.dart | 1 - app/lib/settings/pages/general.dart | 73 ++++++------------ app/lib/settings/pages/inputs.dart | 56 ++++++-------- app/lib/settings/pages/logs.dart | 1 - app/lib/settings/pages/personalization.dart | 77 +++++++++---------- app/lib/settings/pages/view.dart | 1 - app/pubspec.lock | 4 +- app/pubspec.yaml | 2 +- 12 files changed, 87 insertions(+), 132 deletions(-) diff --git a/app/lib/settings/pages/behaviors/home.dart b/app/lib/settings/pages/behaviors/home.dart index 2cc0b15dbbee..7f53b71581a8 100644 --- a/app/lib/settings/pages/behaviors/home.dart +++ b/app/lib/settings/pages/behaviors/home.dart @@ -1,7 +1,6 @@ part of '../../home.dart'; final _behaviorsSettingsPage = SettingsLeapPage( - id: 'behaviors', displayName: (context) => AppLocalizations.of(context).behaviors, icon: PhosphorIconsLight.faders, appBarBuilder: _butterflyAppBar, diff --git a/app/lib/settings/pages/behaviors/persistence.dart b/app/lib/settings/pages/behaviors/persistence.dart index fccb775c4dbb..e33da2d330c0 100644 --- a/app/lib/settings/pages/behaviors/persistence.dart +++ b/app/lib/settings/pages/behaviors/persistence.dart @@ -1,7 +1,6 @@ part of '../../home.dart'; final _persistenceSettingsPage = SettingsLeapPage( - id: 'persistence', displayName: (context) => AppLocalizations.of(context).persistenceDocumentStates, icon: PhosphorIconsLight.database, diff --git a/app/lib/settings/pages/connections.dart b/app/lib/settings/pages/connections.dart index 4b0da70f9432..6bd4d0c9904a 100644 --- a/app/lib/settings/pages/connections.dart +++ b/app/lib/settings/pages/connections.dart @@ -1,7 +1,6 @@ part of '../home.dart'; final _connectionsSettingsPage = SettingsLeapPage( - id: 'connections', displayName: (context) => AppLocalizations.of(context).connections, icon: PhosphorIconsLight.cloud, enabled: (context, state) => !kIsWeb, diff --git a/app/lib/settings/pages/data.dart b/app/lib/settings/pages/data.dart index 82356391e122..815fe5d8e6c7 100644 --- a/app/lib/settings/pages/data.dart +++ b/app/lib/settings/pages/data.dart @@ -1,7 +1,6 @@ part of '../home.dart'; final _dataSettingsPage = SettingsLeapPage( - id: 'data', displayName: (context) => AppLocalizations.of(context).data, icon: PhosphorIconsLight.database, appBarBuilder: _butterflyAppBar, diff --git a/app/lib/settings/pages/experiments.dart b/app/lib/settings/pages/experiments.dart index ab2e7ce4e2f4..e0b1f9648833 100644 --- a/app/lib/settings/pages/experiments.dart +++ b/app/lib/settings/pages/experiments.dart @@ -1,7 +1,6 @@ part of '../home.dart'; final _experimentsSettingsPage = SettingsLeapPage( - id: 'experiments', displayName: (context) => AppLocalizations.of(context).experiments, icon: PhosphorIconsLight.flask, keywordsBuilder: (context) => [ diff --git a/app/lib/settings/pages/general.dart b/app/lib/settings/pages/general.dart index c9f067e8320f..229cd3004f28 100644 --- a/app/lib/settings/pages/general.dart +++ b/app/lib/settings/pages/general.dart @@ -1,7 +1,6 @@ part of '../home.dart'; final _generalSettingsPage = SettingsLeapPage( - id: 'general', displayName: (context) => AppLocalizations.of(context).general, icon: PhosphorIconsLight.gear, appBarBuilder: _butterflyAppBar, @@ -17,7 +16,7 @@ final _generalSettingsPage = SettingsLeapPage( displayName: (context) => AppLocalizations.of(context).checkForUpdates, enabled: (context, state) => !kIsWeb, - builder: _updateCheckSetting, + builder: (context, state) => const _UpdateCheckSetting(), ), ], ), @@ -26,7 +25,7 @@ final _generalSettingsPage = SettingsLeapPage( SettingsLeapActionSetting( displayName: (context) => AppLocalizations.of(context).documentation, icon: PhosphorIconsLight.article, - onTap: _openDocumentation, + onTap: (context) => _openUrl(Uri.https('butterfly.linwood.dev', '')), ), SettingsLeapActionSetting( displayName: (context) => AppLocalizations.of(context).releaseNotes, @@ -36,27 +35,30 @@ final _generalSettingsPage = SettingsLeapPage( SettingsLeapActionSetting( displayName: (context) => 'Matrix', icon: PhosphorIconsLight.users, - onTap: _openMatrix, + onTap: (context) => _openUrl(Uri.https('go.linwood.dev', 'matrix')), ), SettingsLeapActionSetting( displayName: (context) => 'Discord', icon: PhosphorIconsLight.users, - onTap: _openDiscord, + onTap: (context) => _openUrl(Uri.https('go.linwood.dev', 'discord')), ), SettingsLeapActionSetting( displayName: (context) => AppLocalizations.of(context).translate, icon: PhosphorIconsLight.translate, - onTap: _openTranslate, + onTap: (context) => + _openUrl(Uri.https('go.linwood.dev', 'butterfly/translate')), ), SettingsLeapActionSetting( displayName: (context) => AppLocalizations.of(context).sourceCode, icon: PhosphorIconsLight.code, - onTap: _openSourceCode, + onTap: (context) => + _openUrl(Uri.https('go.linwood.dev', 'butterfly/source')), ), SettingsLeapActionSetting( displayName: (context) => AppLocalizations.of(context).changelog, icon: PhosphorIconsLight.arrowCounterClockwise, - onTap: _openChangelog, + onTap: (context) => + _openUrl(Uri.https('butterfly.linwood.dev', 'changelog')), ), ], ), @@ -65,17 +67,19 @@ final _generalSettingsPage = SettingsLeapPage( SettingsLeapActionSetting( displayName: (context) => AppLocalizations.of(context).license, icon: PhosphorIconsLight.stack, - onTap: _openLicense, + onTap: (context) => + _openUrl(Uri.https('go.linwood.dev', 'butterfly/license')), ), SettingsLeapActionSetting( displayName: (context) => AppLocalizations.of(context).imprint, icon: PhosphorIconsLight.identificationCard, - onTap: _openImprint, + onTap: (context) => _openUrl(Uri.https('go.linwood.dev', 'imprint')), ), SettingsLeapActionSetting( displayName: (context) => AppLocalizations.of(context).privacypolicy, icon: PhosphorIconsLight.shield, - onTap: _openPrivacyPolicy, + onTap: (context) => + _openUrl(Uri.https('butterfly.linwood.dev', 'privacypolicy')), ), SettingsLeapActionSetting( displayName: (context) => @@ -134,9 +138,11 @@ class _UpdateCheckSettingState extends State<_UpdateCheckSetting> { Future<_Meta>? _metaFuture; final Future _currentVersion = getCurrentVersion(); - void _loadMeta() => setState(() { - _metaFuture = _fetchMeta(); - }); + void _loadMeta() { + setState(() { + _metaFuture = _fetchMeta(); + }); + } Future<_Meta> _fetchMeta() async { final response = await http.get( @@ -172,6 +178,7 @@ class _UpdateCheckSettingState extends State<_UpdateCheckSetting> { onTap: _loadMeta, ); } + final meta = snapshot.data!; final stableVersion = meta.stableVersion; final nightlyVersion = meta.nightlyVersion; @@ -183,6 +190,7 @@ class _UpdateCheckSettingState extends State<_UpdateCheckSetting> { meta.nightlyVersion == '?' || meta.stableVersion == '?'; final isUpdateAvailable = !isError && !isStable && !isNightly && !isDevelop && !isMain; + return Column( children: [ ListTile( @@ -213,7 +221,9 @@ class _UpdateCheckSettingState extends State<_UpdateCheckSetting> { title: Text(AppLocalizations.of(context).updateAvailable), subtitle: Text(AppLocalizations.of(context).updateNow), leading: const PhosphorIcon(PhosphorIconsLight.arrowRight), - onTap: _openDownloads, + onTap: () => _openUrl( + Uri.parse('https://butterfly.linwood.dev/downloads'), + ), ), ], ); @@ -224,38 +234,5 @@ class _UpdateCheckSettingState extends State<_UpdateCheckSetting> { } } -Widget _updateCheckSetting(BuildContext context, ButterflySettings state) => - const _UpdateCheckSetting(); - Future _openUrl(Uri uri) => launchUrl(uri, mode: LaunchMode.externalApplication); - -void _openDocumentation(BuildContext context) => - _openUrl(Uri.https('butterfly.linwood.dev', '')); - -void _openMatrix(BuildContext context) => - _openUrl(Uri.https('go.linwood.dev', 'matrix')); - -void _openDiscord(BuildContext context) => - _openUrl(Uri.https('go.linwood.dev', 'discord')); - -void _openTranslate(BuildContext context) => - _openUrl(Uri.https('go.linwood.dev', 'butterfly/translate')); - -void _openSourceCode(BuildContext context) => - _openUrl(Uri.https('go.linwood.dev', 'butterfly/source')); - -void _openChangelog(BuildContext context) => - _openUrl(Uri.https('butterfly.linwood.dev', 'changelog')); - -void _openLicense(BuildContext context) => - _openUrl(Uri.https('go.linwood.dev', 'butterfly/license')); - -void _openImprint(BuildContext context) => - _openUrl(Uri.https('go.linwood.dev', 'imprint')); - -void _openPrivacyPolicy(BuildContext context) => - _openUrl(Uri.https('butterfly.linwood.dev', 'privacypolicy')); - -void _openDownloads() => - _openUrl(Uri.parse('https://butterfly.linwood.dev/downloads')); diff --git a/app/lib/settings/pages/inputs.dart b/app/lib/settings/pages/inputs.dart index 1706076ceecc..fb600fa8e8df 100644 --- a/app/lib/settings/pages/inputs.dart +++ b/app/lib/settings/pages/inputs.dart @@ -1,7 +1,6 @@ part of '../home.dart'; final _inputsSettingsPage = SettingsLeapPage( - id: 'inputs', displayName: (context) => AppLocalizations.of(context).inputs, icon: PhosphorIconsLight.keyboard, appBarBuilder: _butterflyAppBar, @@ -17,22 +16,22 @@ final _inputsSettingsPage = SettingsLeapPage( SettingsLeapActionSetting( displayName: (context) => AppLocalizations.of(context).mouse, icon: PhosphorIconsLight.mouse, - onTap: _openMouseSettings, + onTap: (context) => context.push('/settings/inputs/mouse'), ), SettingsLeapActionSetting( displayName: (context) => AppLocalizations.of(context).touch, icon: PhosphorIconsLight.hand, - onTap: _openTouchSettings, + onTap: (context) => context.push('/settings/inputs/touch'), ), SettingsLeapActionSetting( displayName: (context) => AppLocalizations.of(context).keyboard, icon: PhosphorIconsLight.keyboard, - onTap: _openKeyboardSettings, + onTap: (context) => context.push('/settings/inputs/keyboard'), ), SettingsLeapActionSetting( displayName: (context) => AppLocalizations.of(context).pen, icon: PhosphorIconsLight.pen, - onTap: _openPenSettings, + onTap: (context) => context.push('/settings/inputs/pen'), ), ], ), @@ -65,8 +64,8 @@ final _inputsSettingsPage = SettingsLeapPage( ), }, ); + final _mouseSettingsPage = SettingsLeapPage( - id: 'mouse', displayName: (context) => AppLocalizations.of(context).mouse, icon: PhosphorIconsLight.mouse, appBarBuilder: _butterflyAppBar, @@ -77,8 +76,8 @@ final _mouseSettingsPage = SettingsLeapPage( ), }, ); + final _touchSettingsPage = SettingsLeapPage( - id: 'touch', displayName: (context) => AppLocalizations.of(context).touch, icon: PhosphorIconsLight.hand, appBarBuilder: _butterflyAppBar, @@ -89,8 +88,8 @@ final _touchSettingsPage = SettingsLeapPage( ), }, ); + final _keyboardSettingsPage = SettingsLeapPage( - id: 'keyboard', displayName: (context) => AppLocalizations.of(context).keyboard, icon: PhosphorIconsLight.keyboard, appBarBuilder: _butterflyAppBar, @@ -101,8 +100,8 @@ final _keyboardSettingsPage = SettingsLeapPage( ), }, ); + final _penSettingsPage = SettingsLeapPage( - id: 'pen', displayName: (context) => AppLocalizations.of(context).pen, icon: PhosphorIconsLight.pen, appBarBuilder: _butterflyAppBar, @@ -114,18 +113,6 @@ final _penSettingsPage = SettingsLeapPage( }, ); -void _openMouseSettings(BuildContext context) => - context.push('/settings/inputs/mouse'); - -void _openTouchSettings(BuildContext context) => - context.push('/settings/inputs/touch'); - -void _openKeyboardSettings(BuildContext context) => - context.push('/settings/inputs/keyboard'); - -void _openPenSettings(BuildContext context) => - context.push('/settings/inputs/pen'); - Widget _selectSensitivitySetting( BuildContext context, ButterflySettings state, @@ -196,7 +183,9 @@ Widget _pointerTestSection( BuildContext context, ButterflySettings state, Widget child, -) => const _PointerTest(); +) { + return const _PointerTest(); +} void _changeFlag(BuildContext context, String flag, bool enabled) { final cubit = context.read(); @@ -220,17 +209,18 @@ class __PointerTestState extends State<_PointerTest> { double? _pressure, _pressureMin, _pressureMax; Color? _pressed; - void Function(PointerEvent event) _changeInputTest(Color? color) => - (PointerEvent event) { - setState(() { - _kind = event.kind; - _buttons = event.buttons; - _pressure = event.pressure; - _pressureMin = event.pressureMin; - _pressureMax = event.pressureMax; - _pressed = color; - }); - }; + void Function(PointerEvent event) _changeInputTest(Color? color) { + return (PointerEvent event) { + setState(() { + _kind = event.kind; + _buttons = event.buttons; + _pressure = event.pressure; + _pressureMin = event.pressureMin; + _pressureMax = event.pressureMax; + _pressed = color; + }); + }; + } @override Widget build(BuildContext context) { diff --git a/app/lib/settings/pages/logs.dart b/app/lib/settings/pages/logs.dart index 07b261708e40..c70a83b26033 100644 --- a/app/lib/settings/pages/logs.dart +++ b/app/lib/settings/pages/logs.dart @@ -1,7 +1,6 @@ part of '../home.dart'; final _logsSettingsPage = SettingsLeapPage( - id: 'logs', displayName: (context) => AppLocalizations.of(context).logs, icon: PhosphorIconsLight.bug, appBarBuilder: _butterflyAppBar, diff --git a/app/lib/settings/pages/personalization.dart b/app/lib/settings/pages/personalization.dart index c3d402356a05..c34a83a5b834 100644 --- a/app/lib/settings/pages/personalization.dart +++ b/app/lib/settings/pages/personalization.dart @@ -1,7 +1,6 @@ part of '../home.dart'; final _personalizationSettingsPage = SettingsLeapPage( - id: 'personalization', displayName: (context) => AppLocalizations.of(context).personalization, icon: PhosphorIconsLight.monitor, appBarBuilder: _butterflyAppBar, @@ -15,11 +14,28 @@ final _personalizationSettingsPage = SettingsLeapPage( read: (state) => state.theme, write: (context, value) => context.read().changeTheme(value, context), - valueLabel: _themeName, + valueLabel: (context, value) => switch (value) { + ThemeMode.system => AppLocalizations.of(context).systemTheme, + ThemeMode.light => AppLocalizations.of(context).lightTheme, + ThemeMode.dark => AppLocalizations.of(context).darkTheme, + }, ), SettingsLeapCustomSetting( displayName: (context) => AppLocalizations.of(context).design, - builder: _designSetting, + builder: (context, state) { + final design = state.design; + return ListTile( + leading: const PhosphorIcon(PhosphorIconsLight.palette), + title: Text(AppLocalizations.of(context).design), + subtitle: Text( + design.isEmpty + ? AppLocalizations.of(context).systemTheme + : design, + ), + trailing: ThemeBox(theme: getThemeData(state.design, false)), + onTap: () => _openDesignModal(context), + ); + }, ), SettingsLeapCustomSetting( displayName: (context) => AppLocalizations.of(context).locale, @@ -32,7 +48,11 @@ final _personalizationSettingsPage = SettingsLeapPage( read: (state) => state.platformTheme, write: (context, value) => context.read().changePlatformTheme(value), - valueLabel: _platformThemeName, + valueLabel: (context, value) => switch (value) { + PlatformTheme.system => AppLocalizations.of(context).systemTheme, + PlatformTheme.desktop => AppLocalizations.of(context).desktop, + PlatformTheme.mobile => AppLocalizations.of(context).mobile, + }, ), SettingsLeapEnumSetting( displayName: (context) => AppLocalizations.of(context).density, @@ -41,7 +61,18 @@ final _personalizationSettingsPage = SettingsLeapPage( read: (state) => state.density, write: (context, value) => context.read().changeDensity(value), - valueLabel: _densityName, + valueLabel: (context, value) => switch (value) { + ThemeDensity.system => AppLocalizations.of(context).systemTheme, + ThemeDensity.maximize => AppLocalizations.of( + context, + ).densityMaximize, + ThemeDensity.desktop => AppLocalizations.of(context).desktop, + ThemeDensity.compact => AppLocalizations.of(context).compact, + ThemeDensity.standard => AppLocalizations.of(context).standard, + ThemeDensity.comfortable => AppLocalizations.of( + context, + ).comfortable, + }, ), SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).highContrast, @@ -63,46 +94,10 @@ final _personalizationSettingsPage = SettingsLeapPage( }, ); -String _themeName(BuildContext context, ThemeMode mode) => switch (mode) { - ThemeMode.system => AppLocalizations.of(context).systemTheme, - ThemeMode.light => AppLocalizations.of(context).lightTheme, - ThemeMode.dark => AppLocalizations.of(context).darkTheme, -}; - -String _platformThemeName(BuildContext context, PlatformTheme theme) => - switch (theme) { - PlatformTheme.system => AppLocalizations.of(context).systemTheme, - PlatformTheme.desktop => AppLocalizations.of(context).desktop, - PlatformTheme.mobile => AppLocalizations.of(context).mobile, - }; - String _localeName(BuildContext context, String locale) => locale.isNotEmpty ? LocaleNames.of(context)?.nameOf(locale.replaceAll('-', '_')) ?? locale : AppLocalizations.of(context).systemLocale; -String _densityName(BuildContext context, ThemeDensity density) => - switch (density) { - ThemeDensity.system => AppLocalizations.of(context).systemTheme, - ThemeDensity.maximize => AppLocalizations.of(context).densityMaximize, - ThemeDensity.desktop => AppLocalizations.of(context).desktop, - ThemeDensity.compact => AppLocalizations.of(context).compact, - ThemeDensity.standard => AppLocalizations.of(context).standard, - ThemeDensity.comfortable => AppLocalizations.of(context).comfortable, - }; - -Widget _designSetting(BuildContext context, ButterflySettings state) { - final design = state.design; - return ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.palette), - title: Text(AppLocalizations.of(context).design), - subtitle: Text( - design.isEmpty ? AppLocalizations.of(context).systemTheme : design, - ), - trailing: ThemeBox(theme: getThemeData(state.design, false)), - onTap: () => _openDesignModal(context), - ); -} - Widget _localeSetting(BuildContext context, ButterflySettings state) { return ListTile( leading: const PhosphorIcon(PhosphorIconsLight.translate), diff --git a/app/lib/settings/pages/view.dart b/app/lib/settings/pages/view.dart index eb9bc7de3b1e..8efcd8fa8772 100644 --- a/app/lib/settings/pages/view.dart +++ b/app/lib/settings/pages/view.dart @@ -1,7 +1,6 @@ part of '../home.dart'; final _viewSettingsPage = SettingsLeapPage( - id: 'view', displayName: (context) => AppLocalizations.of(context).view, icon: PhosphorIconsLight.eye, appBarBuilder: _butterflyAppBar, diff --git a/app/pubspec.lock b/app/pubspec.lock index a3d4702ff3d2..7a524a751d47 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -1274,8 +1274,8 @@ packages: dependency: "direct main" description: path: "packages/settings_leap" - ref: "81142414ae23372173056f4e56ba893fe2ff47e4" - resolved-ref: "81142414ae23372173056f4e56ba893fe2ff47e4" + ref: "09c2e7eb9ad1cc82f5a72a92ceb13668b638685a" + resolved-ref: "09c2e7eb9ad1cc82f5a72a92ceb13668b638685a" url: "https://github.com/LinwoodDev/dart_pkgs.git" source: git version: "0.1.0" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index f768a6e92977..8c3b252af7c1 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: settings_leap: git: url: https://github.com/LinwoodDev/dart_pkgs.git - ref: 81142414ae23372173056f4e56ba893fe2ff47e4 + ref: 09c2e7eb9ad1cc82f5a72a92ceb13668b638685a path: packages/settings_leap material_leap: git: From a815d1c04ec0418fc060ca10c2486f64dcac4b95 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 5 Jul 2026 22:03:39 +0200 Subject: [PATCH 046/117] Migrate input settings to settings leap, add descriptions for settings --- app/lib/l10n/app_en.arb | 129 +++- app/lib/main.dart | 13 +- app/lib/settings/home.dart | 22 +- app/lib/settings/inputs/keyboard.dart | 273 ------- app/lib/settings/inputs/mouse.dart | 227 ------ app/lib/settings/inputs/pen.dart | 395 ---------- app/lib/settings/inputs/shortcut.dart | 97 --- app/lib/settings/inputs/touch.dart | 118 --- app/lib/settings/pages/behaviors/home.dart | 151 +++- .../settings/pages/behaviors/persistence.dart | 32 + app/lib/settings/pages/data.dart | 13 + app/lib/settings/pages/experiments.dart | 6 + app/lib/settings/pages/inputs.dart | 716 +++++++++++++++++- app/lib/settings/pages/logs.dart | 2 + app/lib/settings/pages/personalization.dart | 147 ++-- app/lib/settings/pages/view.dart | 248 ++---- app/lib/widgets/input_mapping_list_tile.dart | 40 - app/pubspec.lock | 4 +- app/pubspec.yaml | 2 +- 19 files changed, 1164 insertions(+), 1471 deletions(-) delete mode 100644 app/lib/settings/inputs/keyboard.dart delete mode 100644 app/lib/settings/inputs/mouse.dart delete mode 100644 app/lib/settings/inputs/pen.dart delete mode 100644 app/lib/settings/inputs/shortcut.dart delete mode 100644 app/lib/settings/inputs/touch.dart delete mode 100644 app/lib/widgets/input_mapping_list_tile.dart diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index c03bf076ca34..a61ea3900780 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -9,9 +9,16 @@ "darkTheme": "Dark theme", "lightTheme": "Light theme", "systemTheme": "Use default system theme", + "designDescription": "Choose the visual theme design", "view": "View", "contentViewport": "Content Viewport", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Limit Viewport to positive coordinates", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Off", "canvas": "Canvas", "interface": "Interface", @@ -108,6 +115,7 @@ }, "locale": "Locale", "systemLocale": "System locale", + "localeDescription": "Choose the app language", "information": "Information", "license": "License", "imprint": "Imprint", @@ -577,6 +585,9 @@ "bottomLeft": "Bottom left", "bottomRight": "Bottom right", "zoomPosition": "Zoom control position", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Caches", "manage": "Manage", "@manage": { @@ -636,24 +647,37 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Input gestures", + "inputGesturesDescription": "Enable touch gestures for moving and zooming the canvas", + "gestureSensitivityDescription": "Adjust how fast two-finger pan and zoom gestures move the canvas", + "touchSensitivityDescription": "Increase this to make touch targets easier to hit", + "selectSensitivityDescription": "Increase this to make element selection easier", + "scrollSensitivityDescription": "Adjust how fast mouse wheel scrolling moves or zooms the canvas", "nativeTitleBar": "Native title bar", + "nativeTitleBarDescription": "Use the operating system title bar on desktop", "mode": "Mode", "syncMode": "Sync mode", + "syncModeDescription": "Choose when remote files sync", "connection": "Connection", "always": "Always", "@always": { "description": "Frequency" }, "noMobile": "No mobile", + "syncModeAlwaysDescription": "Sync automatically whenever files change", + "syncModeNoMobileDescription": "Sync automatically except on mobile devices", "manual": "Manual", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only sync when you trigger it manually", "search": "Search", "@search": { "description": "Search action" }, "properties": "Properties", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Pin", "@pin": { "description": "Pin action" @@ -942,6 +966,9 @@ "direction": "Direction", "onlyAvailableLargerScreen": "Only available on larger screens", "toolbarPosition": "Toolbar position", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Rotate", "@rotate": { "description": "Rotate action" @@ -951,6 +978,9 @@ "description": "Spacer element" }, "navigationRail": "Navigation rail", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Cut", "@cut": { "description": "Clipboard action" @@ -966,6 +996,7 @@ "ascending": "Ascending", "descending": "Descending", "imageScale": "Image scale", + "imageScaleDescription": "Scale imported images when their size is detected automatically", "svgScale": "SVG scale", "noImageSelected": "No image selected", "noSvgSelected": "No SVG selected", @@ -982,6 +1013,9 @@ "description": "Texture property" }, "platformTheme": "Platform theme", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Desktop", "@desktop": { "description": "Platform" @@ -1006,6 +1040,9 @@ }, "iceServers": "ICE Servers", "collaboration": "Collaboration", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Web Socket", "iceServer": "ICE Server", @@ -1037,7 +1074,7 @@ "hideUI": "Hide UI", "density": "Density", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Compact", "@compact": { @@ -1109,14 +1146,23 @@ }, "continueAnyway": "Continue anyway", "zoomControl": "Zoom control", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "High contrast", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "This value should be valid number", "createAreas": "Create areas", "autosave": "Autosave", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Invert", "@invert": { "description": "Invert action" @@ -1131,10 +1177,19 @@ "description": "Size value" }, "toolbarSize": "Toolbar size", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Add all", "onlyCurrentPage": "Only current page", "smoothNavigation": "Smooth navigation", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Edge pan area switching", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Use Android SAF", "exact": "Exact", "@exact": { @@ -1145,6 +1200,9 @@ "description": "Inline position" }, "toolbarRows": "Toolbar rows", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Pointer test", "pressure": "Pressure", "small": "Small", @@ -1158,6 +1216,9 @@ "selectAll": "Select all", "overrideTools": "Override tools", "hideCursorWhileDrawing": "Hide cursor while drawing", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Installed", "install": "Install", "@install": { @@ -1174,6 +1235,9 @@ "description": "Scroll action" }, "onStartup": "On startup", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Home screen", "lastNote": "Last note", "newNote": "New note", @@ -1301,10 +1365,21 @@ "description": "Math tool" }, "ignorePressure": "Ignore pressure", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, "ignoreFirstPressureDescription": "On some devices, the first pressure value is not accurate. This setting will ignore the first pressure value and use the pressure of the second event instead.", + "ignorePressureNeverDescription": "Use every pressure value from the device", + "ignorePressureAlwaysDescription": "Ignore pressure and treat the pen as fully pressed", "temporary": "Temporary", "simpleToolbarVisibility": "Simple toolbar visibility", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Autosave delay", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Saved", "@saved": { "description": "Save status" @@ -1392,5 +1467,55 @@ "type": "int" } } - } + }, + "zoomControlDescription": "Show zoom controls in the canvas view", + "zoomPositionDescription": "Choose where the zoom controls appear", + "propertiesDescription": "Choose where the properties panel appears", + "toolbarPositionDescription": "Choose where the main toolbar appears", + "toolbarSizeDescription": "Choose how large the toolbar buttons are", + "toolbarRowsDescription": "Set how many rows the toolbar can use", + "navigationRailDescription": "Show a navigation rail on wider screens", + "navigatorPositionDescription": "Choose which side the navigator appears on", + "optionsPanelPositionDescription": "Choose whether the options panel appears above or below the canvas", + "simpleToolbarVisibilityDescription": "Choose when the simplified toolbar is shown", + "showThumbnailsDescription": "Show thumbnails for notes in the file list", + "hideFileExtensionDescription": "Hide file extensions in file names", + "highContrastDescription": "Use stronger contrast in the app theme", + "autosaveDescription": "Choose how changes are saved", + "autosaveEnabledDescription": "Save changes automatically as you work", + "autosaveDelayedDescription": "Save changes automatically after a short delay", + "autosaveShowButtonDescription": "Show a save button and save manually", + "autosaveDisabledDescription": "Turn off automatic saving", + "autosaveDelayDescription": "Wait time before delayed autosave runs", + "hideCursorWhileDrawingDescription": "Hide the mouse cursor while drawing", + "onStartupDescription": "Choose what opens when the app starts", + "onStartupHomeScreenDescription": "Open the home screen when the app starts", + "onStartupLastNoteDescription": "Reopen the most recent note when the app starts", + "onStartupNewNoteDescription": "Create a new note when the app starts", + "smoothNavigationDescription": "Reduce the amount of rendering work while navigating", + "edgePanAreaSwitchingDescription": "Switch areas when you pan near the edge of the canvas", + "collaborationDescription": "Allow multiple people to edit the same note together", + "densityDescription": "Choose how compact the interface should be", + "showVerboseLogsDescription": "Include debug and verbose logs in the log view and console", + "bringMovedElementsToFrontDescription": "Move dragged elements in front of other elements", + "persistenceDocumentStatesDescription": "Choose which document state details are stored between sessions", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limit how far the viewport can move beyond the content", + "limitViewportPositiveDescription": "Prevent the viewport from moving into negative coordinates", + "startInFullScreenDescription": "Open the app in full screen", + "platformThemeDescription": "Choose whether the app follows the system, desktop, or mobile layout", + "penOnlyInputDescription": "Choose when the app should ignore touch and mouse input", + "showPenOnlyToggleDescription": "Show the pen only toggle button when a stylus is detected", + "ignorePressureDescription": "Choose how stylus pressure is handled", + "moveOnGestureDescription": "Move the canvas when multi-touch gestures are used", + "spreadPagesDescription": "Split imported content across multiple pages" } diff --git a/app/lib/main.dart b/app/lib/main.dart index b5e20e3a066f..f82f8d21f008 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -135,17 +135,22 @@ class ButterflyApp extends StatelessWidget { SettingsLeapTree tree, ) { List buildEntries( - Map> entries, - ) { + Map> entries, [ + String? parentId, + ]) { return entries.entries.map((entry) { final id = entry.key; + final fullId = parentId == null ? id : '$parentId.$id'; final page = entry.value; final children = page.children; return GoRoute( path: id, - builder: (context, state) => SettingsDetailsPage(id: id), + builder: (context, state) => SettingsDetailsPage( + id: fullId, + focusedId: state.extra is String ? state.extra as String : null, + ), routes: [ - ...buildEntries(children), + ...buildEntries(children, fullId), if (id == 'connections') GoRoute( path: ':id', diff --git a/app/lib/settings/home.dart b/app/lib/settings/home.dart index ab83660ddd0c..b14768ee5141 100644 --- a/app/lib/settings/home.dart +++ b/app/lib/settings/home.dart @@ -3,17 +3,15 @@ import 'dart:io'; import 'dart:math' as math; import 'dart:ui'; +import 'package:butterfly/actions/shortcuts.dart'; import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/api/open.dart'; import 'package:butterfly/cubits/settings.dart'; +import 'package:butterfly/dialogs/input.dart'; import 'package:butterfly/main.dart'; import 'package:butterfly/repositories/document_state.dart'; import 'package:butterfly/services/logger.dart'; import 'package:butterfly/settings/data.dart'; -import 'package:butterfly/settings/inputs/keyboard.dart'; -import 'package:butterfly/settings/inputs/mouse.dart'; -import 'package:butterfly/settings/inputs/pen.dart'; -import 'package:butterfly/settings/inputs/touch.dart'; import 'package:butterfly/theme.dart'; import 'package:butterfly/visualizer/connection.dart'; import 'package:file_picker/file_picker.dart'; @@ -27,6 +25,7 @@ import 'package:go_router/go_router.dart'; import 'package:html/parser.dart' as html_parser; import 'package:image/image.dart' as img; import 'package:intl/intl.dart' show DateFormat; +import 'package:keybinder/keybinder.dart'; import 'package:lw_file_system/lw_file_system.dart'; import 'package:material_leap/material_leap.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; @@ -72,6 +71,9 @@ class SettingsPage extends StatelessWidget { searchHint: (context) => AppLocalizations.of(context).search, isDialog: inView, compactWidth: LeapBreakpoints.compact, + onOpenPage: (context, id, page, focusedId) { + context.go('/settings/${id.replaceAll('.', '/')}', extra: focusedId); + }, closeButton: IconButton.outlined( icon: const PhosphorIcon(PhosphorIconsLight.x), onPressed: () => Navigator.of(context).maybePop(), @@ -94,9 +96,15 @@ class SettingsPage extends StatelessWidget { class SettingsDetailsPage extends StatelessWidget { final String id; + final String? focusedId; final bool inView; - const SettingsDetailsPage({super.key, required this.id, this.inView = false}); + const SettingsDetailsPage({ + super.key, + required this.id, + this.focusedId, + this.inView = false, + }); @override Widget build(BuildContext context) { @@ -113,7 +121,7 @@ class SettingsDetailsPage extends StatelessWidget { return null; } - final page = findPage(settingsTree.pages, id); + final page = settingsTree.pageById(id) ?? findPage(settingsTree.pages, id); if (page == null) { return Scaffold( appBar: WindowTitleBar( @@ -126,6 +134,8 @@ class SettingsDetailsPage extends StatelessWidget { return BlocBuilder( builder: (context, state) => SettingsLeapGeneratedPage( page: page, + pageId: id, + focusedId: focusedId, state: state, inView: inView, cardMargin: settingsCardMargin, diff --git a/app/lib/settings/inputs/keyboard.dart b/app/lib/settings/inputs/keyboard.dart deleted file mode 100644 index 69be24ffaf4e..000000000000 --- a/app/lib/settings/inputs/keyboard.dart +++ /dev/null @@ -1,273 +0,0 @@ -import 'package:butterfly/actions/shortcuts.dart'; -import 'package:butterfly/api/open.dart'; -import 'package:butterfly/theme.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:butterfly/src/generated/i18n/app_localizations.dart'; -import 'package:phosphor_flutter/phosphor_flutter.dart'; -import 'package:keybinder/keybinder.dart'; -import 'package:butterfly/dialogs/input.dart'; - -import '../../cubits/settings.dart'; - -class KeyboardInputSettings extends StatelessWidget { - final ButterflySettings state; - const KeyboardInputSettings({super.key, required this.state}); - - @override - Widget build(BuildContext context) { - final generalShortcuts = [ - newShortcut, - newFromTemplateShortcut, - exportShortcut, - exportTextShortcut, - imageExportShortcut, - pdfExportShortcut, - svgExportShortcut, - packsShortcut, - settingsShortcut, - exitShortcut, - ]; - - final projectShortcuts = [ - searchShortcut, - undoShortcut, - redoShortcut, - backgroundShortcut, - saveShortcut, - changePathShortcut, - zoomInShortcut, - zoomOutShortcut, - fullScreenShortcut, - hideUIShortcut, - nextShortcut, - previousShortcut, - nextPageShortcut, - previousPageShortcut, - togglePresentationShortcut, - selectAllShortcut, - pasteShortcut, - ...changeToolShortcuts, - ]; - - return ListenableBuilder( - listenable: keybinder, - builder: (context, _) => Column( - children: [ - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: ListTile( - title: Text(AppLocalizations.of(context).shortcuts), - leading: const PhosphorIcon(PhosphorIconsLight.keyboard), - onTap: () => openHelp(['shortcuts'], 'keyboard'), - trailing: const PhosphorIcon(PhosphorIconsLight.arrowSquareOut), - ), - ), - ), - const SizedBox(height: 16), - _buildHoldShortcutsSection(context, state.inputConfiguration), - const SizedBox(height: 16), - _buildSection( - context, - AppLocalizations.of(context).general, - generalShortcuts, - ), - const SizedBox(height: 16), - _buildSection(context, 'Project', projectShortcuts), - const SizedBox(height: 16), - ], - ), - ); - } - - Widget _buildHoldShortcutsSection( - BuildContext context, - InputConfiguration config, - ) { - return Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: settingsCardTitlePadding, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - AppLocalizations.of(context).holdShortcuts, - style: TextTheme.of(context).headlineSmall, - ), - const SizedBox(height: 4), - Text( - AppLocalizations.of(context).holdShortcutsDescription, - style: TextTheme.of(context).bodyMedium?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - IconButton( - icon: const PhosphorIcon(PhosphorIconsLight.plus), - onPressed: () { - context.read().changeInputConfiguration( - config.copyWith( - holdShortcuts: [ - ...config.holdShortcuts, - const HoldShortcut( - keyId: 0, - mapping: InputMapping(InputMapping.handToolValue), - ), - ], - ), - ); - }, - ), - ], - ), - ), - const SizedBox(height: 16), - ...config.holdShortcuts.asMap().entries.map((entry) { - final index = entry.key; - final shortcut = entry.value; - return Row( - children: [ - Expanded( - child: ListTile( - title: Text(shortcut.mapping.getDescription(context)), - subtitle: Text(AppLocalizations.of(context).action), - onTap: () { - openInputMappingModal( - context, - AppLocalizations.of(context).action, - shortcut.mapping, - (mapping) { - final newShortcuts = List.from( - config.holdShortcuts, - ); - newShortcuts[index] = shortcut.copyWith( - mapping: mapping, - ); - context - .read() - .changeInputConfiguration( - config.copyWith(holdShortcuts: newShortcuts), - ); - }, - ); - }, - ), - ), - Expanded( - child: KeyRecorderListTile( - title: Text(AppLocalizations.of(context).key), - currentActivator: SingleActivator( - LogicalKeyboardKey(shortcut.keyId), - ), - onNewKey: (activator) { - final newShortcuts = List.from( - config.holdShortcuts, - ); - newShortcuts[index] = shortcut.copyWith( - keyId: activator.trigger.keyId, - ); - context.read().changeInputConfiguration( - config.copyWith(holdShortcuts: newShortcuts), - ); - }, - ), - ), - IconButton( - icon: const PhosphorIcon(PhosphorIconsLight.trash), - onPressed: () { - final newShortcuts = List.from( - config.holdShortcuts, - ); - newShortcuts.removeAt(index); - context.read().changeInputConfiguration( - config.copyWith(holdShortcuts: newShortcuts), - ); - }, - ), - ], - ); - }), - ], - ), - ), - ); - } - - Widget _buildSection( - BuildContext context, - String title, - List shortcuts, - ) { - return Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: settingsCardTitlePadding, - child: Text(title, style: TextTheme.of(context).headlineSmall), - ), - const SizedBox(height: 16), - LayoutBuilder( - builder: (context, constraints) { - final width = constraints.maxWidth; - final columns = width > 600 ? 2 : 1; - final itemWidth = width / columns; - - return Wrap( - children: shortcuts - .map( - (e) => SizedBox( - width: itemWidth, - child: _buildShortcutTile( - context, - e, - e.getLocalizedName(context), - ), - ), - ) - .toList(), - ); - }, - ), - ], - ), - ), - ); - } - - Widget _buildShortcutTile( - BuildContext context, - ShortcutDefinition def, - String title, - ) { - return KeyRecorderListTile( - title: Text(title), - currentActivator: keybinder.getActivator(def.id), - onNewKey: (newKey) => keybinder.updateBinding(def.id, newKey), - // Only set reset if it has not default - onReset: keybinder.getActivator(def.id) != def.defaultActivator - ? () => keybinder.resetBinding(def.id) - : null, - ); - } -} diff --git a/app/lib/settings/inputs/mouse.dart b/app/lib/settings/inputs/mouse.dart deleted file mode 100644 index 2e32bc2052b8..000000000000 --- a/app/lib/settings/inputs/mouse.dart +++ /dev/null @@ -1,227 +0,0 @@ -import 'package:butterfly/api/open.dart'; -import 'package:butterfly/theme.dart'; -import 'package:butterfly/widgets/input_mapping_list_tile.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:butterfly/src/generated/i18n/app_localizations.dart'; -import 'package:phosphor_flutter/phosphor_flutter.dart'; - -import '../../cubits/settings.dart'; -import 'shortcut.dart'; - -class MouseInputSettings extends StatelessWidget { - final ButterflySettings state; - const MouseInputSettings({super.key, required this.state}); - - String _getDoubleName(BuildContext context, String inputName) => - '${AppLocalizations.of(context).double} $inputName'; - - String _getTripleName(String inputName) => 'Triple $inputName'; - - @override - Widget build(BuildContext context) { - final config = state.inputConfiguration; - final availableShortcuts = getInputShortcutOptions(context); - return Column( - children: [ - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - children: [ - SwitchListTile( - value: state.hideCursorWhileDrawing, - title: Text( - AppLocalizations.of(context).hideCursorWhileDrawing, - ), - secondary: const PhosphorIcon(PhosphorIconsLight.cursorClick), - onChanged: (value) => context - .read() - .changeHideCursorWhileDrawing(value), - ), - ], - ), - ), - ), - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: settingsCardTitlePadding, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context).shortcuts, - style: TextTheme.of(context).headlineSmall, - ), - IconButton( - icon: const PhosphorIcon( - PhosphorIconsLight.sealQuestion, - ), - tooltip: AppLocalizations.of(context).help, - onPressed: () => openHelp(['shortcuts'], 'configure'), - ), - ], - ), - ), - const SizedBox(height: 16), - InputMappingListTile( - inputName: AppLocalizations.of(context).left, - currentValue: config.leftMouse, - defaultValue: InputMappingDefault.leftMouse, - icon: const PhosphorIcon( - PhosphorIconsLight.mouseLeftClick, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(leftMouse: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getDoubleName( - context, - AppLocalizations.of(context).left, - ), - currentValue: config.doubleLeftMouseShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.mouseLeftClick, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(doubleLeftMouseShortcut: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getTripleName(AppLocalizations.of(context).left), - currentValue: config.tripleLeftMouseShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.mouseLeftClick, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(tripleLeftMouseShortcut: value), - ); - }, - ), - InputMappingListTile( - inputName: AppLocalizations.of(context).middle, - currentValue: config.middleMouse, - defaultValue: InputMappingDefault.middleMouse, - icon: const PhosphorIcon( - PhosphorIconsLight.mouseMiddleClick, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(middleMouse: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getDoubleName( - context, - AppLocalizations.of(context).middle, - ), - currentValue: config.doubleMiddleMouseShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.mouseMiddleClick, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(doubleMiddleMouseShortcut: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getTripleName( - AppLocalizations.of(context).middle, - ), - currentValue: config.tripleMiddleMouseShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.mouseMiddleClick, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(tripleMiddleMouseShortcut: value), - ); - }, - ), - InputMappingListTile( - inputName: AppLocalizations.of(context).right, - currentValue: config.rightMouse, - defaultValue: InputMappingDefault.rightMouse, - icon: const PhosphorIcon( - PhosphorIconsLight.mouseRightClick, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(rightMouse: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getDoubleName( - context, - AppLocalizations.of(context).right, - ), - currentValue: config.doubleRightMouseShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.mouseRightClick, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(doubleRightMouseShortcut: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getTripleName(AppLocalizations.of(context).right), - currentValue: config.tripleRightMouseShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.mouseRightClick, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(tripleRightMouseShortcut: value), - ); - }, - ), - ], - ), - ), - ), - ], - ); - } -} diff --git a/app/lib/settings/inputs/pen.dart b/app/lib/settings/inputs/pen.dart deleted file mode 100644 index c520ded360c1..000000000000 --- a/app/lib/settings/inputs/pen.dart +++ /dev/null @@ -1,395 +0,0 @@ -import 'package:butterfly/theme.dart'; -import 'package:butterfly/widgets/input_mapping_list_tile.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:butterfly/src/generated/i18n/app_localizations.dart'; -import 'package:material_leap/material_leap.dart'; -import 'package:phosphor_flutter/phosphor_flutter.dart'; - -import '../../api/open.dart'; -import '../../cubits/settings.dart'; -import 'shortcut.dart'; - -class PenInputSettings extends StatelessWidget { - final ButterflySettings state; - const PenInputSettings({super.key, required this.state}); - - String _getDoubleName(BuildContext context, String inputName) => - '${AppLocalizations.of(context).double} $inputName'; - - String _getTripleName(String inputName) => 'Triple $inputName'; - - String _getPenOnlyInputName(bool? value, BuildContext context) { - if (value == null) return AppLocalizations.of(context).automatic; - return value - ? AppLocalizations.of(context).alwaysOn - : AppLocalizations.of(context).alwaysOff; - } - - String _getIgnorePressureName( - IgnorePressure ignorePressure, - BuildContext context, - ) => switch (ignorePressure) { - IgnorePressure.never => AppLocalizations.of(context).never, - IgnorePressure.first => AppLocalizations.of(context).first, - IgnorePressure.always => AppLocalizations.of(context).always, - }; - - String? _getIgnorePressureDescription( - IgnorePressure ignorePressure, - BuildContext context, - ) => switch (ignorePressure) { - IgnorePressure.first => AppLocalizations.of( - context, - ).ignoreFirstPressureDescription, - _ => null, - }; - - @override - Widget build(BuildContext context) { - final config = state.inputConfiguration; - final availableShortcuts = getInputShortcutOptions(context); - return Column( - children: [ - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ListTile( - title: Text(AppLocalizations.of(context).penOnlyInput), - subtitle: Text( - _getPenOnlyInputName(state.penOnlyInput, context), - ), - leading: const PhosphorIcon( - PhosphorIconsLight.pencilSimpleLine, - ), - onTap: () { - final cubit = context.read(); - final current = cubit.state.penOnlyInput; - - showLeapBottomSheet( - context: context, - titleBuilder: (context) => - Text(AppLocalizations.of(context).penOnlyInput), - childrenBuilder: (context) { - return [ - ListTile( - title: Text(AppLocalizations.of(context).automatic), - subtitle: Text( - AppLocalizations.of( - context, - ).penOnlyInputAutoDescription, - ), - selected: current == null, - onTap: () { - cubit.changePenOnlyInput(null); - Navigator.of(context).pop(); - }, - ), - ListTile( - title: Text(AppLocalizations.of(context).alwaysOn), - subtitle: Text( - AppLocalizations.of( - context, - ).penOnlyInputOnDescription, - ), - selected: current == true, - onTap: () { - cubit.changePenOnlyInput(true); - Navigator.of(context).pop(); - }, - ), - ListTile( - title: Text(AppLocalizations.of(context).alwaysOff), - subtitle: Text( - AppLocalizations.of( - context, - ).penOnlyInputOffDescription, - ), - selected: current == false, - onTap: () { - cubit.changePenOnlyInput(false); - Navigator.of(context).pop(); - }, - ), - ]; - }, - ); - }, - ), - SwitchListTile( - value: state.showPenOnlyToggle, - title: Text(AppLocalizations.of(context).showPenOnlyToggle), - secondary: const PhosphorIcon(PhosphorIconsLight.toggleRight), - onChanged: (value) => context - .read() - .changeShowPenOnlyToggle(value), - ), - ListTile( - title: Text(AppLocalizations.of(context).ignorePressure), - subtitle: Text( - _getIgnorePressureName(state.ignorePressure, context), - ), - leading: const PhosphorIcon(PhosphorIconsLight.lineSegments), - onTap: () { - final cubit = context.read(); - final ignorePressure = cubit.state.ignorePressure; - - showLeapBottomSheet( - context: context, - titleBuilder: (context) => - Text(AppLocalizations.of(context).ignorePressure), - childrenBuilder: (context) { - return [ - ...IgnorePressure.values.map((e) { - final description = _getIgnorePressureDescription( - e, - context, - ); - return ListTile( - title: Text(_getIgnorePressureName(e, context)), - subtitle: description != null - ? Text(description) - : null, - selected: e == ignorePressure, - onTap: () { - cubit.changeIgnorePressure(e); - Navigator.of(context).pop(); - }, - ); - }), - ]; - }, - ); - }, - ), - ], - ), - ), - ), - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: settingsCardTitlePadding, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context).shortcuts, - style: TextTheme.of(context).headlineSmall, - ), - IconButton( - icon: const PhosphorIcon( - PhosphorIconsLight.sealQuestion, - ), - tooltip: AppLocalizations.of(context).help, - onPressed: () => openHelp(['shortcuts'], 'configure'), - ), - ], - ), - ), - const SizedBox(height: 16), - InputMappingListTile( - inputName: AppLocalizations.of(context).pen, - currentValue: config.pen, - defaultValue: InputMappingDefault.pen, - icon: const PhosphorIcon(PhosphorIconsLight.pen), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration(config.copyWith(pen: value)); - }, - ), - InputShortcutListTile( - inputName: _getDoubleName( - context, - AppLocalizations.of(context).pen, - ), - currentValue: config.doublePenShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon(PhosphorIconsLight.pen), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(doublePenShortcut: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getTripleName(AppLocalizations.of(context).pen), - currentValue: config.triplePenShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon(PhosphorIconsLight.pen), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(triplePenShortcut: value), - ); - }, - ), - InputMappingListTile( - inputName: AppLocalizations.of(context).invertedPen, - currentValue: config.invertedPen, - defaultValue: InputMappingDefault.invertedPen, - icon: Transform.flip( - flipX: true, - flipY: true, - child: PhosphorIcon(PhosphorIconsLight.pen), - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(invertedPen: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getDoubleName( - context, - AppLocalizations.of(context).invertedPen, - ), - currentValue: config.doubleInvertedPenShortcut, - availableShortcuts: availableShortcuts, - icon: Transform.flip( - flipX: true, - flipY: true, - child: PhosphorIcon(PhosphorIconsLight.pen), - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(doubleInvertedPenShortcut: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getTripleName( - AppLocalizations.of(context).invertedPen, - ), - currentValue: config.tripleInvertedPenShortcut, - availableShortcuts: availableShortcuts, - icon: Transform.flip( - flipX: true, - flipY: true, - child: PhosphorIcon(PhosphorIconsLight.pen), - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(tripleInvertedPenShortcut: value), - ); - }, - ), - InputMappingListTile( - inputName: AppLocalizations.of(context).first, - currentValue: config.firstPenButton, - defaultValue: InputMappingDefault.firstPenButton, - icon: const PhosphorIcon( - PhosphorIconsLight.numberCircleOne, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(firstPenButton: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getDoubleName( - context, - AppLocalizations.of(context).first, - ), - currentValue: config.doubleFirstPenButtonShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.numberCircleOne, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(doubleFirstPenButtonShortcut: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getTripleName(AppLocalizations.of(context).first), - currentValue: config.tripleFirstPenButtonShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.numberCircleOne, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(tripleFirstPenButtonShortcut: value), - ); - }, - ), - InputMappingListTile( - inputName: AppLocalizations.of(context).second, - currentValue: config.secondPenButton, - defaultValue: InputMappingDefault.secondPenButton, - icon: const PhosphorIcon( - PhosphorIconsLight.numberCircleTwo, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(secondPenButton: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getDoubleName( - context, - AppLocalizations.of(context).second, - ), - currentValue: config.doubleSecondPenButtonShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.numberCircleTwo, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(doubleSecondPenButtonShortcut: value), - ); - }, - ), - InputShortcutListTile( - inputName: _getTripleName( - AppLocalizations.of(context).second, - ), - currentValue: config.tripleSecondPenButtonShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon( - PhosphorIconsLight.numberCircleTwo, - textDirection: TextDirection.ltr, - ), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(tripleSecondPenButtonShortcut: value), - ); - }, - ), - ], - ), - ), - ), - ], - ); - } -} diff --git a/app/lib/settings/inputs/shortcut.dart b/app/lib/settings/inputs/shortcut.dart deleted file mode 100644 index 0ea7887acac5..000000000000 --- a/app/lib/settings/inputs/shortcut.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:butterfly/actions/shortcuts.dart'; -import 'package:butterfly/src/generated/i18n/app_localizations.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:material_leap/material_leap.dart'; -import 'package:phosphor_flutter/phosphor_flutter.dart'; - -List<(String?, String)> getInputShortcutOptions(BuildContext context) { - final projectShortcuts = [ - searchShortcut, - undoShortcut, - redoShortcut, - backgroundShortcut, - saveShortcut, - changePathShortcut, - zoomInShortcut, - zoomOutShortcut, - fullScreenShortcut, - hideUIShortcut, - nextPageShortcut, - previousPageShortcut, - selectAllShortcut, - pasteShortcut, - ...changeToolShortcuts, - ]; - - return [ - (null, AppLocalizations.of(context).none), - ('long_press', AppLocalizations.of(context).longPress), - ...projectShortcuts.map((e) => (e.id, e.getLocalizedName(context))), - ]; -} - -void showInputShortcutPicker( - BuildContext context, - String title, - List<(String?, String)> availableShortcuts, - String? currentValue, - ValueChanged onChanged, -) { - showLeapBottomSheet( - context: context, - titleBuilder: (ctx) => Text(title), - childrenBuilder: (ctx) => [ - RadioGroup( - groupValue: currentValue, - onChanged: (val) { - onChanged(val); - Navigator.of(context).pop(); - }, - child: Column( - children: availableShortcuts - .map( - (e) => RadioListTile(value: e.$1, title: Text(e.$2)), - ) - .toList(), - ), - ), - ], - ); -} - -class InputShortcutListTile extends StatelessWidget { - final String inputName; - final String? currentValue; - final List<(String?, String)> availableShortcuts; - final Widget? icon; - final ValueChanged onChanged; - - const InputShortcutListTile({ - super.key, - required this.inputName, - required this.currentValue, - required this.availableShortcuts, - required this.onChanged, - this.icon, - }); - - @override - Widget build(BuildContext context) { - return ListTile( - leading: icon ?? const PhosphorIcon(PhosphorIconsLight.keyboard), - title: Text(inputName), - subtitle: Text( - availableShortcuts.firstWhereOrNull((e) => e.$1 == currentValue)?.$2 ?? - AppLocalizations.of(context).none, - ), - onTap: () => showInputShortcutPicker( - context, - inputName, - availableShortcuts, - currentValue, - onChanged, - ), - ); - } -} diff --git a/app/lib/settings/inputs/touch.dart b/app/lib/settings/inputs/touch.dart deleted file mode 100644 index 72283c6a075e..000000000000 --- a/app/lib/settings/inputs/touch.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'package:butterfly/theme.dart'; -import 'package:butterfly/widgets/input_mapping_list_tile.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:butterfly/src/generated/i18n/app_localizations.dart'; -import 'package:phosphor_flutter/phosphor_flutter.dart'; - -import '../../api/open.dart'; -import '../../cubits/settings.dart'; -import 'shortcut.dart'; - -class TouchInputSettings extends StatelessWidget { - final ButterflySettings state; - const TouchInputSettings({super.key, required this.state}); - - @override - Widget build(BuildContext context) { - final availableShortcuts = getInputShortcutOptions(context); - - final config = state.inputConfiguration; - return Column( - children: [ - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SwitchListTile( - value: state.inputGestures, - title: Text(AppLocalizations.of(context).inputGestures), - secondary: const PhosphorIcon(PhosphorIconsLight.handTap), - onChanged: (value) => - context.read().changeInputGestures(value), - ), - SwitchListTile( - value: state.moveOnGesture, - title: Text(AppLocalizations.of(context).moveOnGesture), - secondary: const PhosphorIcon( - PhosphorIconsLight.arrowsOutCardinal, - ), - onChanged: (value) => - context.read().changeMoveOnGesture(value), - ), - ], - ), - ), - ), - Card( - margin: settingsCardMargin, - child: Padding( - padding: settingsCardPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: settingsCardTitlePadding, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context).shortcuts, - style: TextTheme.of(context).headlineSmall, - ), - IconButton( - icon: const PhosphorIcon( - PhosphorIconsLight.sealQuestion, - ), - tooltip: AppLocalizations.of(context).help, - onPressed: () => openHelp(['shortcuts'], 'configure'), - ), - ], - ), - ), - const SizedBox(height: 16), - InputMappingListTile( - inputName: AppLocalizations.of(context).touch, - currentValue: config.touch, - defaultValue: InputMappingDefault.touch, - icon: const PhosphorIcon(PhosphorIconsLight.handPointing), - onChanged: (value) { - final cubit = context.read(); - cubit.changeInputConfiguration( - config.copyWith(touch: value), - ); - }, - ), - InputShortcutListTile( - inputName: AppLocalizations.of(context).doublePressAction, - currentValue: config.doubleTouchShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon(PhosphorIconsLight.handTap), - onChanged: (value) { - context.read().changeInputConfiguration( - config.copyWith(doubleTouchShortcut: value), - ); - }, - ), - InputShortcutListTile( - inputName: AppLocalizations.of(context).triplePressAction, - currentValue: config.tripleTouchShortcut, - availableShortcuts: availableShortcuts, - icon: const PhosphorIcon(PhosphorIconsLight.handTap), - onChanged: (value) { - context.read().changeInputConfiguration( - config.copyWith(tripleTouchShortcut: value), - ); - }, - ), - ], - ), - ), - ), - ], - ); - } -} diff --git a/app/lib/settings/pages/behaviors/home.dart b/app/lib/settings/pages/behaviors/home.dart index 7f53b71581a8..fd0e6c8d84e6 100644 --- a/app/lib/settings/pages/behaviors/home.dart +++ b/app/lib/settings/pages/behaviors/home.dart @@ -8,47 +8,147 @@ final _behaviorsSettingsPage = SettingsLeapPage( sections: { 'behavior': SettingsLeapSection( settings: [ - SettingsLeapCustomSetting( + SettingsLeapListSetting( + id: 'autosave', displayName: (context) => AppLocalizations.of(context).autosave, + descriptionBuilder: (context) => + AppLocalizations.of(context).autosaveDescription, + icon: PhosphorIconsLight.floppyDisk, keywordsBuilder: (context) => [AppLocalizations.of(context).save], - builder: _autosaveSetting, + options: [ + SettingsLeapOption( + id: 'enabled', + value: _AutosaveMode.enabled, + displayName: (context) => AppLocalizations.of(context).yes, + descriptionBuilder: (context) => + AppLocalizations.of(context).autosaveEnabledDescription, + ), + SettingsLeapOption( + id: 'delayed', + value: _AutosaveMode.delayed, + displayName: (context) => AppLocalizations.of(context).delay, + descriptionBuilder: (context) => + AppLocalizations.of(context).autosaveDelayedDescription, + ), + SettingsLeapOption( + id: 'showSaveButton', + value: _AutosaveMode.showSaveButton, + displayName: (context) => + AppLocalizations.of(context).yesButShowButtons, + descriptionBuilder: (context) => + AppLocalizations.of(context).autosaveShowButtonDescription, + ), + SettingsLeapOption( + id: 'disabled', + value: _AutosaveMode.disabled, + displayName: (context) => AppLocalizations.of(context).no, + descriptionBuilder: (context) => + AppLocalizations.of(context).autosaveDisabledDescription, + ), + ], + read: _readAutosaveMode, + write: (context, value) => switch (value) { + _AutosaveMode.enabled => + context.read().changeAutosave(true), + _AutosaveMode.delayed => + context.read().changeAutosave(null, delayed: true), + _AutosaveMode.showSaveButton => + context.read().changeAutosave(null), + _AutosaveMode.disabled => + context.read().changeAutosave(false), + }, ), SettingsLeapCustomSetting( displayName: (context) => AppLocalizations.of(context).autosaveDelay, + descriptionBuilder: (context) => + AppLocalizations.of(context).autosaveDelayDescription, enabled: (context, state) => state.autosave && state.delayedAutosave, builder: _autosaveDelaySetting, ), SettingsLeapEnumSetting( displayName: (context) => AppLocalizations.of(context).onStartup, + descriptionBuilder: (context) => + AppLocalizations.of(context).onStartupDescription, icon: PhosphorIconsLight.arrowFatLineUp, values: StartupBehavior.values, read: (state) => state.onStartup, write: (context, value) => context.read().changeStartupBehavior(value), valueLabel: _startupBehaviorName, + valueDescription: (context, value) => switch (value) { + StartupBehavior.openHomeScreen => AppLocalizations.of( + context, + ).onStartupHomeScreenDescription, + StartupBehavior.openLastNote => AppLocalizations.of( + context, + ).onStartupLastNoteDescription, + StartupBehavior.openNewNote => AppLocalizations.of( + context, + ).onStartupNewNoteDescription, + }, ), SettingsLeapActionSetting( displayName: (context) => AppLocalizations.of(context).persistenceDocumentStates, + descriptionBuilder: (context) => + AppLocalizations.of(context).persistenceDocumentStatesDescription, icon: PhosphorIconsLight.database, onTap: _openPersistenceSettings, ), SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).startInFullScreen, + descriptionBuilder: (context) => + AppLocalizations.of(context).startInFullScreenDescription, icon: PhosphorIconsLight.arrowsOut, read: (state) => state.startInFullScreen, write: (context, value) => context.read().changeStartInFullScreen(value), ), - SettingsLeapCustomSetting( + SettingsLeapListSetting( + id: 'contentViewport', displayName: (context) => AppLocalizations.of(context).contentViewport, - builder: _contentViewportSetting, + descriptionBuilder: (context) => + AppLocalizations.of(context).contentViewportDescription, + icon: PhosphorIconsLight.appWindow, + options: [ + SettingsLeapOption( + id: 'off', + value: null, + displayName: (context) => AppLocalizations.of(context).off, + ), + SettingsLeapOption( + id: '1x', + value: 1, + displayName: (context) => '1x', + ), + SettingsLeapOption( + id: '1.5x', + value: 1.5, + displayName: (context) => '1.5x', + ), + SettingsLeapOption( + id: '2x', + value: 2, + displayName: (context) => '2x', + ), + SettingsLeapOption( + id: '3x', + value: 3, + displayName: (context) => '3x', + ), + ], + read: (state) => state.limitViewportMultiplier, + write: (context, value) => context + .read() + .changeLimitViewportMultiplier(value), ), SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).limitViewportToPositiveCoordinates, + descriptionBuilder: (context) => + AppLocalizations.of(context).limitViewportPositiveDescription, icon: PhosphorIconsLight.plusSquare, read: (state) => state.limitViewportPositive, write: (context, value) => @@ -63,10 +163,23 @@ final _behaviorsSettingsPage = SettingsLeapPage( write: (context, value) => context.read().changeRenderResolution(value), valueLabel: _renderResolutionName, + valueDescription: (context, value) => switch (value) { + RenderResolution.performance => AppLocalizations.of( + context, + ).performanceDescription, + RenderResolution.normal => AppLocalizations.of( + context, + ).normalDescription, + RenderResolution.high => AppLocalizations.of( + context, + ).highDescription, + }, ), SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).bringMovedElementsToFront, + descriptionBuilder: (context) => + AppLocalizations.of(context).bringMovedElementsToFrontDescription, icon: PhosphorIconsLight.stack, read: (state) => state.bringMovedElementsToFront, write: (context, value) => context @@ -80,6 +193,8 @@ final _behaviorsSettingsPage = SettingsLeapPage( settings: [ SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).spreadToPages, + descriptionBuilder: (context) => + AppLocalizations.of(context).spreadPagesDescription, icon: PhosphorIconsLight.arrowsOutSimple, read: (state) => state.spreadPages, write: (context, value) => @@ -87,6 +202,8 @@ final _behaviorsSettingsPage = SettingsLeapPage( ), SettingsLeapCustomSetting( displayName: (context) => AppLocalizations.of(context).imageScale, + descriptionBuilder: (context) => + AppLocalizations.of(context).imageScaleDescription, builder: _imageScaleSetting, ), ], @@ -94,6 +211,15 @@ final _behaviorsSettingsPage = SettingsLeapPage( }, ); +enum _AutosaveMode { enabled, delayed, showSaveButton, disabled } + +_AutosaveMode _readAutosaveMode(ButterflySettings state) { + if (!state.autosave) return _AutosaveMode.disabled; + if (state.delayedAutosave) return _AutosaveMode.delayed; + if (state.showSaveButton) return _AutosaveMode.showSaveButton; + return _AutosaveMode.enabled; +} + String _renderResolutionName(BuildContext context, RenderResolution value) => switch (value) { RenderResolution.performance => AppLocalizations.of(context).performance, @@ -101,23 +227,6 @@ String _renderResolutionName(BuildContext context, RenderResolution value) => RenderResolution.high => AppLocalizations.of(context).high, }; -Widget _autosaveSetting(BuildContext context, ButterflySettings state) { - return ListTile( - title: Text(AppLocalizations.of(context).autosave), - leading: const PhosphorIcon(PhosphorIconsLight.floppyDisk), - subtitle: Text( - state.autosave - ? state.delayedAutosave - ? AppLocalizations.of(context).delay - : state.showSaveButton - ? AppLocalizations.of(context).yesButShowButtons - : AppLocalizations.of(context).yes - : AppLocalizations.of(context).no, - ), - onTap: () => _openAutosaveModal(context), - ); -} - Widget _autosaveDelaySetting(BuildContext context, ButterflySettings state) { return ExactSlider( leading: const PhosphorIcon(PhosphorIconsLight.clock), diff --git a/app/lib/settings/pages/behaviors/persistence.dart b/app/lib/settings/pages/behaviors/persistence.dart index e33da2d330c0..ebd61b000df9 100644 --- a/app/lib/settings/pages/behaviors/persistence.dart +++ b/app/lib/settings/pages/behaviors/persistence.dart @@ -24,6 +24,9 @@ Widget _buildPersistenceSettingsPage( value: settings.enabled, secondary: const PhosphorIcon(PhosphorIconsLight.power), title: Text(AppLocalizations.of(context).persistentStatesEnabled), + subtitle: Text( + AppLocalizations.of(context).persistentStatesEnabledDescription, + ), onChanged: (value) => change(settings.copyWith(enabled: value)), ), const Divider(), @@ -31,6 +34,9 @@ Widget _buildPersistenceSettingsPage( value: settings.page, secondary: const PhosphorIcon(PhosphorIconsLight.file), title: Text(AppLocalizations.of(context).persistentStateCurrentPage), + subtitle: Text( + AppLocalizations.of(context).persistentStateCurrentPageDescription, + ), onChanged: settings.enabled ? (value) => change(settings.copyWith(page: value)) : null, @@ -39,6 +45,9 @@ Widget _buildPersistenceSettingsPage( value: settings.camera, secondary: const PhosphorIcon(PhosphorIconsLight.frameCorners), title: Text(AppLocalizations.of(context).persistentStateViewport), + subtitle: Text( + AppLocalizations.of(context).persistentStateViewportDescription, + ), onChanged: settings.enabled ? (value) => change(settings.copyWith(camera: value)) : null, @@ -47,6 +56,9 @@ Widget _buildPersistenceSettingsPage( value: settings.locks, secondary: const PhosphorIcon(PhosphorIconsLight.lockKey), title: Text(AppLocalizations.of(context).lock), + subtitle: Text( + AppLocalizations.of(context).persistentStateLocksDescription, + ), onChanged: settings.enabled ? (value) => change(settings.copyWith(locks: value)) : null, @@ -55,6 +67,9 @@ Widget _buildPersistenceSettingsPage( value: settings.tool, secondary: const PhosphorIcon(PhosphorIconsLight.toolbox), title: Text(AppLocalizations.of(context).persistentStateSelectedTool), + subtitle: Text( + AppLocalizations.of(context).persistentStateSelectedToolDescription, + ), onChanged: settings.enabled ? (value) => change(settings.copyWith(tool: value)) : null, @@ -63,6 +78,9 @@ Widget _buildPersistenceSettingsPage( value: settings.navigator, secondary: const PhosphorIcon(PhosphorIconsLight.sidebar), title: Text(AppLocalizations.of(context).navigator), + subtitle: Text( + AppLocalizations.of(context).persistentStateNavigatorDescription, + ), onChanged: settings.enabled ? (value) => change(settings.copyWith(navigator: value)) : null, @@ -71,6 +89,9 @@ Widget _buildPersistenceSettingsPage( value: settings.layers, secondary: const PhosphorIcon(PhosphorIconsLight.stack), title: Text(AppLocalizations.of(context).layers), + subtitle: Text( + AppLocalizations.of(context).persistentStateLayersDescription, + ), onChanged: settings.enabled ? (value) => change(settings.copyWith(layers: value)) : null, @@ -79,6 +100,9 @@ Widget _buildPersistenceSettingsPage( value: settings.areas, secondary: const PhosphorIcon(PhosphorIconsLight.selection), title: Text(AppLocalizations.of(context).areas), + subtitle: Text( + AppLocalizations.of(context).persistentStateAreasDescription, + ), onChanged: settings.enabled ? (value) => change(settings.copyWith(areas: value)) : null, @@ -86,6 +110,9 @@ Widget _buildPersistenceSettingsPage( const Divider(), ExactSlider( header: Text(AppLocalizations.of(context).persistentStateMaxRecords), + subtitle: Text( + AppLocalizations.of(context).persistentStateMaxRecordsDescription, + ), leading: const PhosphorIcon(PhosphorIconsLight.listNumbers), value: settings.maxEntries.toDouble(), min: 20, @@ -99,6 +126,11 @@ Widget _buildPersistenceSettingsPage( header: Text( AppLocalizations.of(context).persistentStateDeleteOlderThanDays, ), + subtitle: Text( + AppLocalizations.of( + context, + ).persistentStateDeleteOlderThanDaysDescription, + ), leading: const PhosphorIcon(PhosphorIconsLight.calendar), value: settings.maxAgeDays.toDouble(), min: 7, diff --git a/app/lib/settings/pages/data.dart b/app/lib/settings/pages/data.dart index 815fe5d8e6c7..3a0d6f64c213 100644 --- a/app/lib/settings/pages/data.dart +++ b/app/lib/settings/pages/data.dart @@ -9,6 +9,8 @@ final _dataSettingsPage = SettingsLeapPage( settings: [ SettingsLeapEnumSetting( displayName: (context) => AppLocalizations.of(context).syncMode, + descriptionBuilder: (context) => + AppLocalizations.of(context).syncModeDescription, icon: PhosphorIconsLight.cloudArrowDown, enabled: (context, state) => !kIsWeb, values: SyncMode.values, @@ -16,6 +18,17 @@ final _dataSettingsPage = SettingsLeapPage( write: (context, value) => context.read().changeSyncMode(value), valueLabel: (context, value) => value.getLocalizedName(context), + valueDescription: (context, value) => switch (value) { + SyncMode.always => AppLocalizations.of( + context, + ).syncModeAlwaysDescription, + SyncMode.noMobile => AppLocalizations.of( + context, + ).syncModeNoMobileDescription, + SyncMode.manual => AppLocalizations.of( + context, + ).syncModeManualDescription, + }, ), SettingsLeapCustomSetting( displayName: (context) => AppLocalizations.of(context).dataDirectory, diff --git a/app/lib/settings/pages/experiments.dart b/app/lib/settings/pages/experiments.dart index e0b1f9648833..779bdb4b4c56 100644 --- a/app/lib/settings/pages/experiments.dart +++ b/app/lib/settings/pages/experiments.dart @@ -16,6 +16,8 @@ final _experimentsSettingsPage = SettingsLeapPage( settings: [ SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).collaboration, + descriptionBuilder: (context) => + AppLocalizations.of(context).collaborationDescription, icon: PhosphorIconsLight.chatTeardrop, read: (state) => state.hasFlag('collaboration'), write: (context, value) => @@ -24,6 +26,8 @@ final _experimentsSettingsPage = SettingsLeapPage( SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).smoothNavigation, + descriptionBuilder: (context) => + AppLocalizations.of(context).smoothNavigationDescription, icon: PhosphorIconsLight.caretCircleDoubleDown, read: (state) => state.hasFlag('smoothNavigation'), write: (context, value) => @@ -32,6 +36,8 @@ final _experimentsSettingsPage = SettingsLeapPage( SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).edgePanAreaSwitching, + descriptionBuilder: (context) => + AppLocalizations.of(context).edgePanAreaSwitchingDescription, icon: PhosphorIconsLight.cursor, read: (state) => state.hasFlag('edgePanAreaSwitching'), write: (context, value) => diff --git a/app/lib/settings/pages/inputs.dart b/app/lib/settings/pages/inputs.dart index fb600fa8e8df..d485d2619fe8 100644 --- a/app/lib/settings/pages/inputs.dart +++ b/app/lib/settings/pages/inputs.dart @@ -1,5 +1,9 @@ part of '../home.dart'; +typedef _InputConfigurationRead = V Function(InputConfiguration config); +typedef _InputConfigurationWrite = + InputConfiguration Function(InputConfiguration config, V value); + final _inputsSettingsPage = SettingsLeapPage( displayName: (context) => AppLocalizations.of(context).inputs, icon: PhosphorIconsLight.keyboard, @@ -42,18 +46,26 @@ final _inputsSettingsPage = SettingsLeapPage( settings: [ SettingsLeapCustomSetting( displayName: (context) => AppLocalizations.of(context).select, + descriptionBuilder: (context) => + AppLocalizations.of(context).selectSensitivityDescription, builder: _selectSensitivitySetting, ), SettingsLeapCustomSetting( displayName: (context) => AppLocalizations.of(context).touch, + descriptionBuilder: (context) => + AppLocalizations.of(context).touchSensitivityDescription, builder: _touchSensitivitySetting, ), SettingsLeapCustomSetting( displayName: (context) => AppLocalizations.of(context).inputGestures, + descriptionBuilder: (context) => + AppLocalizations.of(context).gestureSensitivityDescription, builder: _gestureSensitivitySetting, ), SettingsLeapCustomSetting( displayName: (context) => AppLocalizations.of(context).scroll, + descriptionBuilder: (context) => + AppLocalizations.of(context).scrollSensitivityDescription, builder: _scrollSensitivitySetting, ), ], @@ -70,9 +82,101 @@ final _mouseSettingsPage = SettingsLeapPage( icon: PhosphorIconsLight.mouse, appBarBuilder: _butterflyAppBar, sections: { - 'content': SettingsLeapSection( - wrapBuilder: false, - builder: (context, state, inView) => MouseInputSettings(state: state), + 'behavior': SettingsLeapSection( + settings: [ + SettingsLeapBoolSetting( + id: 'hideCursorWhileDrawing', + displayName: (context) => + AppLocalizations.of(context).hideCursorWhileDrawing, + descriptionBuilder: (context) => + AppLocalizations.of(context).hideCursorWhileDrawingDescription, + icon: PhosphorIconsLight.cursorClick, + read: (state) => state.hideCursorWhileDrawing, + write: (context, value) => + context.read().changeHideCursorWhileDrawing(value), + ), + ], + ), + 'shortcuts': SettingsLeapSection( + displayName: (context) => AppLocalizations.of(context).shortcuts, + headerBuilder: _shortcutsHelpHeader, + settings: [ + _inputMappingSetting( + id: 'leftMouse', + displayName: (context) => AppLocalizations.of(context).left, + icon: PhosphorIconsLight.mouseLeftClick, + read: (config) => config.leftMouse, + write: (config, value) => config.copyWith(leftMouse: value), + ), + _inputShortcutSetting( + id: 'doubleLeftMouseShortcut', + displayName: (context) => + _getDoubleName(context, AppLocalizations.of(context).left), + icon: PhosphorIconsLight.mouseLeftClick, + read: (config) => config.doubleLeftMouseShortcut, + write: (config, value) => + config.copyWith(doubleLeftMouseShortcut: value), + ), + _inputShortcutSetting( + id: 'tripleLeftMouseShortcut', + displayName: (context) => + _getTripleName(AppLocalizations.of(context).left), + icon: PhosphorIconsLight.mouseLeftClick, + read: (config) => config.tripleLeftMouseShortcut, + write: (config, value) => + config.copyWith(tripleLeftMouseShortcut: value), + ), + _inputMappingSetting( + id: 'middleMouse', + displayName: (context) => AppLocalizations.of(context).middle, + icon: PhosphorIconsLight.mouseMiddleClick, + read: (config) => config.middleMouse, + write: (config, value) => config.copyWith(middleMouse: value), + ), + _inputShortcutSetting( + id: 'doubleMiddleMouseShortcut', + displayName: (context) => + _getDoubleName(context, AppLocalizations.of(context).middle), + icon: PhosphorIconsLight.mouseMiddleClick, + read: (config) => config.doubleMiddleMouseShortcut, + write: (config, value) => + config.copyWith(doubleMiddleMouseShortcut: value), + ), + _inputShortcutSetting( + id: 'tripleMiddleMouseShortcut', + displayName: (context) => + _getTripleName(AppLocalizations.of(context).middle), + icon: PhosphorIconsLight.mouseMiddleClick, + read: (config) => config.tripleMiddleMouseShortcut, + write: (config, value) => + config.copyWith(tripleMiddleMouseShortcut: value), + ), + _inputMappingSetting( + id: 'rightMouse', + displayName: (context) => AppLocalizations.of(context).right, + icon: PhosphorIconsLight.mouseRightClick, + read: (config) => config.rightMouse, + write: (config, value) => config.copyWith(rightMouse: value), + ), + _inputShortcutSetting( + id: 'doubleRightMouseShortcut', + displayName: (context) => + _getDoubleName(context, AppLocalizations.of(context).right), + icon: PhosphorIconsLight.mouseRightClick, + read: (config) => config.doubleRightMouseShortcut, + write: (config, value) => + config.copyWith(doubleRightMouseShortcut: value), + ), + _inputShortcutSetting( + id: 'tripleRightMouseShortcut', + displayName: (context) => + _getTripleName(AppLocalizations.of(context).right), + icon: PhosphorIconsLight.mouseRightClick, + read: (config) => config.tripleRightMouseShortcut, + write: (config, value) => + config.copyWith(tripleRightMouseShortcut: value), + ), + ], ), }, ); @@ -82,9 +186,58 @@ final _touchSettingsPage = SettingsLeapPage( icon: PhosphorIconsLight.hand, appBarBuilder: _butterflyAppBar, sections: { - 'content': SettingsLeapSection( - wrapBuilder: false, - builder: (context, state, inView) => TouchInputSettings(state: state), + 'behavior': SettingsLeapSection( + settings: [ + SettingsLeapBoolSetting( + id: 'inputGestures', + displayName: (context) => AppLocalizations.of(context).inputGestures, + descriptionBuilder: (context) => + AppLocalizations.of(context).inputGesturesDescription, + icon: PhosphorIconsLight.handTap, + read: (state) => state.inputGestures, + write: (context, value) => + context.read().changeInputGestures(value), + ), + SettingsLeapBoolSetting( + id: 'moveOnGesture', + displayName: (context) => AppLocalizations.of(context).moveOnGesture, + descriptionBuilder: (context) => + AppLocalizations.of(context).moveOnGestureDescription, + icon: PhosphorIconsLight.arrowsOutCardinal, + read: (state) => state.moveOnGesture, + write: (context, value) => + context.read().changeMoveOnGesture(value), + ), + ], + ), + 'shortcuts': SettingsLeapSection( + displayName: (context) => AppLocalizations.of(context).shortcuts, + headerBuilder: _shortcutsHelpHeader, + settings: [ + _inputMappingSetting( + id: 'touch', + displayName: (context) => AppLocalizations.of(context).touch, + icon: PhosphorIconsLight.handPointing, + read: (config) => config.touch, + write: (config, value) => config.copyWith(touch: value), + ), + _inputShortcutSetting( + id: 'doubleTouchShortcut', + displayName: (context) => + AppLocalizations.of(context).doublePressAction, + icon: PhosphorIconsLight.handTap, + read: (config) => config.doubleTouchShortcut, + write: (config, value) => config.copyWith(doubleTouchShortcut: value), + ), + _inputShortcutSetting( + id: 'tripleTouchShortcut', + displayName: (context) => + AppLocalizations.of(context).triplePressAction, + icon: PhosphorIconsLight.handTap, + read: (config) => config.tripleTouchShortcut, + write: (config, value) => config.copyWith(tripleTouchShortcut: value), + ), + ], ), }, ); @@ -94,9 +247,63 @@ final _keyboardSettingsPage = SettingsLeapPage( icon: PhosphorIconsLight.keyboard, appBarBuilder: _butterflyAppBar, sections: { - 'content': SettingsLeapSection( + 'help': SettingsLeapSection( + settings: [ + SettingsLeapActionSetting( + id: 'shortcuts', + displayName: (context) => AppLocalizations.of(context).shortcuts, + icon: PhosphorIconsLight.keyboard, + onTap: (context) => openHelp(['shortcuts'], 'keyboard'), + ), + ], + ), + 'holdShortcuts': SettingsLeapSection( + wrapBuilder: false, + builder: (context, state, child) => + _buildHoldShortcutsSection(context, state.inputConfiguration), + ), + 'general': SettingsLeapSection( + wrapBuilder: false, + builder: (context, state, child) => _buildKeyboardShortcutSection( + context, + AppLocalizations.of(context).general, + [ + newShortcut, + newFromTemplateShortcut, + exportShortcut, + exportTextShortcut, + imageExportShortcut, + pdfExportShortcut, + svgExportShortcut, + packsShortcut, + settingsShortcut, + exitShortcut, + ], + ), + ), + 'project': SettingsLeapSection( wrapBuilder: false, - builder: (context, state, inView) => KeyboardInputSettings(state: state), + builder: (context, state, child) => + _buildKeyboardShortcutSection(context, 'Project', [ + searchShortcut, + undoShortcut, + redoShortcut, + backgroundShortcut, + saveShortcut, + changePathShortcut, + zoomInShortcut, + zoomOutShortcut, + fullScreenShortcut, + hideUIShortcut, + nextShortcut, + previousShortcut, + nextPageShortcut, + previousPageShortcut, + togglePresentationShortcut, + selectAllShortcut, + pasteShortcut, + ...changeToolShortcuts, + ]), ), }, ); @@ -106,13 +313,500 @@ final _penSettingsPage = SettingsLeapPage( icon: PhosphorIconsLight.pen, appBarBuilder: _butterflyAppBar, sections: { - 'content': SettingsLeapSection( - wrapBuilder: false, - builder: (context, state, inView) => PenInputSettings(state: state), + 'behavior': SettingsLeapSection( + settings: [ + SettingsLeapListSetting( + id: 'penOnlyInput', + displayName: (context) => AppLocalizations.of(context).penOnlyInput, + descriptionBuilder: (context) => + AppLocalizations.of(context).penOnlyInputDescription, + icon: PhosphorIconsLight.pencilSimpleLine, + options: [ + SettingsLeapOption( + id: 'automatic', + value: null, + displayName: (context) => AppLocalizations.of(context).automatic, + descriptionBuilder: (context) => + AppLocalizations.of(context).penOnlyInputAutoDescription, + ), + SettingsLeapOption( + id: 'alwaysOn', + value: true, + displayName: (context) => AppLocalizations.of(context).alwaysOn, + descriptionBuilder: (context) => + AppLocalizations.of(context).penOnlyInputOnDescription, + ), + SettingsLeapOption( + id: 'alwaysOff', + value: false, + displayName: (context) => AppLocalizations.of(context).alwaysOff, + descriptionBuilder: (context) => + AppLocalizations.of(context).penOnlyInputOffDescription, + ), + ], + read: (state) => state.penOnlyInput, + write: (context, value) => + context.read().changePenOnlyInput(value), + ), + SettingsLeapBoolSetting( + id: 'showPenOnlyToggle', + displayName: (context) => + AppLocalizations.of(context).showPenOnlyToggle, + descriptionBuilder: (context) => + AppLocalizations.of(context).showPenOnlyToggleDescription, + icon: PhosphorIconsLight.toggleRight, + read: (state) => state.showPenOnlyToggle, + write: (context, value) => + context.read().changeShowPenOnlyToggle(value), + ), + SettingsLeapListSetting( + id: 'ignorePressure', + displayName: (context) => AppLocalizations.of(context).ignorePressure, + descriptionBuilder: (context) => + AppLocalizations.of(context).ignorePressureDescription, + icon: PhosphorIconsLight.lineSegments, + options: [ + for (final value in IgnorePressure.values) + SettingsLeapOption( + id: value.name, + value: value, + displayName: (context) => + _getIgnorePressureName(value, context), + descriptionBuilder: (context) => switch (value) { + IgnorePressure.never => AppLocalizations.of( + context, + ).ignorePressureNeverDescription, + IgnorePressure.first => AppLocalizations.of( + context, + ).ignoreFirstPressureDescription, + IgnorePressure.always => AppLocalizations.of( + context, + ).ignorePressureAlwaysDescription, + }, + ), + ], + read: (state) => state.ignorePressure, + write: (context, value) => + context.read().changeIgnorePressure(value), + ), + ], + ), + 'shortcuts': SettingsLeapSection( + displayName: (context) => AppLocalizations.of(context).shortcuts, + headerBuilder: _shortcutsHelpHeader, + settings: [ + _inputMappingSetting( + id: 'pen', + displayName: (context) => AppLocalizations.of(context).pen, + icon: PhosphorIconsLight.pen, + read: (config) => config.pen, + write: (config, value) => config.copyWith(pen: value), + ), + _inputShortcutSetting( + id: 'doublePenShortcut', + displayName: (context) => + _getDoubleName(context, AppLocalizations.of(context).pen), + icon: PhosphorIconsLight.pen, + read: (config) => config.doublePenShortcut, + write: (config, value) => config.copyWith(doublePenShortcut: value), + ), + _inputShortcutSetting( + id: 'triplePenShortcut', + displayName: (context) => + _getTripleName(AppLocalizations.of(context).pen), + icon: PhosphorIconsLight.pen, + read: (config) => config.triplePenShortcut, + write: (config, value) => config.copyWith(triplePenShortcut: value), + ), + _inputMappingSetting( + id: 'invertedPen', + displayName: (context) => AppLocalizations.of(context).invertedPen, + icon: PhosphorIconsLight.pen, + read: (config) => config.invertedPen, + write: (config, value) => config.copyWith(invertedPen: value), + ), + _inputShortcutSetting( + id: 'doubleInvertedPenShortcut', + displayName: (context) => + _getDoubleName(context, AppLocalizations.of(context).invertedPen), + icon: PhosphorIconsLight.pen, + read: (config) => config.doubleInvertedPenShortcut, + write: (config, value) => + config.copyWith(doubleInvertedPenShortcut: value), + ), + _inputShortcutSetting( + id: 'tripleInvertedPenShortcut', + displayName: (context) => + _getTripleName(AppLocalizations.of(context).invertedPen), + icon: PhosphorIconsLight.pen, + read: (config) => config.tripleInvertedPenShortcut, + write: (config, value) => + config.copyWith(tripleInvertedPenShortcut: value), + ), + _inputMappingSetting( + id: 'firstPenButton', + displayName: (context) => AppLocalizations.of(context).first, + icon: PhosphorIconsLight.numberCircleOne, + read: (config) => config.firstPenButton, + write: (config, value) => config.copyWith(firstPenButton: value), + ), + _inputShortcutSetting( + id: 'doubleFirstPenButtonShortcut', + displayName: (context) => + _getDoubleName(context, AppLocalizations.of(context).first), + icon: PhosphorIconsLight.numberCircleOne, + read: (config) => config.doubleFirstPenButtonShortcut, + write: (config, value) => + config.copyWith(doubleFirstPenButtonShortcut: value), + ), + _inputShortcutSetting( + id: 'tripleFirstPenButtonShortcut', + displayName: (context) => + _getTripleName(AppLocalizations.of(context).first), + icon: PhosphorIconsLight.numberCircleOne, + read: (config) => config.tripleFirstPenButtonShortcut, + write: (config, value) => + config.copyWith(tripleFirstPenButtonShortcut: value), + ), + _inputMappingSetting( + id: 'secondPenButton', + displayName: (context) => AppLocalizations.of(context).second, + icon: PhosphorIconsLight.numberCircleTwo, + read: (config) => config.secondPenButton, + write: (config, value) => config.copyWith(secondPenButton: value), + ), + _inputShortcutSetting( + id: 'doubleSecondPenButtonShortcut', + displayName: (context) => + _getDoubleName(context, AppLocalizations.of(context).second), + icon: PhosphorIconsLight.numberCircleTwo, + read: (config) => config.doubleSecondPenButtonShortcut, + write: (config, value) => + config.copyWith(doubleSecondPenButtonShortcut: value), + ), + _inputShortcutSetting( + id: 'tripleSecondPenButtonShortcut', + displayName: (context) => + _getTripleName(AppLocalizations.of(context).second), + icon: PhosphorIconsLight.numberCircleTwo, + read: (config) => config.tripleSecondPenButtonShortcut, + write: (config, value) => + config.copyWith(tripleSecondPenButtonShortcut: value), + ), + ], ), }, ); +Widget _shortcutsHelpHeader(BuildContext context, ButterflySettings state) { + return Align( + alignment: AlignmentDirectional.centerEnd, + child: IconButton( + icon: const PhosphorIcon(PhosphorIconsLight.sealQuestion), + tooltip: AppLocalizations.of(context).help, + onPressed: () => openHelp(['shortcuts'], 'configure'), + ), + ); +} + +String _getDoubleName(BuildContext context, String inputName) => + '${AppLocalizations.of(context).double} $inputName'; + +String _getTripleName(String inputName) => 'Triple $inputName'; + +String _getIgnorePressureName( + IgnorePressure ignorePressure, + BuildContext context, +) => switch (ignorePressure) { + IgnorePressure.never => AppLocalizations.of(context).never, + IgnorePressure.first => AppLocalizations.of(context).first, + IgnorePressure.always => AppLocalizations.of(context).always, +}; + +SettingsLeapListSetting _inputMappingSetting({ + required String id, + required SettingsLeapDisplayName displayName, + required IconData icon, + required _InputConfigurationRead read, + required _InputConfigurationWrite write, +}) { + return SettingsLeapListSetting( + id: id, + displayName: displayName, + icon: icon, + options: [ + SettingsLeapOption( + id: 'activeTool', + value: const InputMapping(InputMapping.activeToolValue), + displayName: (context) => AppLocalizations.of(context).activeTool, + descriptionBuilder: (context) => + AppLocalizations.of(context).activeToolDescription, + ), + SettingsLeapOption( + id: 'handTool', + value: const InputMapping(InputMapping.handToolValue), + displayName: (context) => AppLocalizations.of(context).handTool, + descriptionBuilder: (context) => + AppLocalizations.of(context).handToolDescription, + ), + for (var index = 0; index < 99; index++) + SettingsLeapOption( + id: 'tool${index + 1}', + value: InputMapping(index), + displayName: (context) => + AppLocalizations.of(context).toolOnToolbarShort(index + 1), + descriptionBuilder: (context) => + AppLocalizations.of(context).toolOnToolbarDescription, + ), + ], + read: (state) => read(state.inputConfiguration), + write: (context, value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + write(cubit.state.inputConfiguration, value), + ); + }, + ); +} + +SettingsLeapListSetting _inputShortcutSetting({ + required String id, + required SettingsLeapDisplayName displayName, + required IconData icon, + required _InputConfigurationRead read, + required _InputConfigurationWrite write, +}) { + return SettingsLeapListSetting( + id: id, + displayName: displayName, + icon: icon, + options: [ + SettingsLeapOption( + id: 'none', + value: null, + displayName: (context) => AppLocalizations.of(context).none, + ), + SettingsLeapOption( + id: 'long_press', + value: 'long_press', + displayName: (context) => AppLocalizations.of(context).longPress, + ), + for (final shortcut in _projectInputShortcuts) + SettingsLeapOption( + id: shortcut.id, + value: shortcut.id, + displayName: (context) => shortcut.getLocalizedName(context), + ), + ], + read: (state) => read(state.inputConfiguration), + write: (context, value) { + final cubit = context.read(); + cubit.changeInputConfiguration( + write(cubit.state.inputConfiguration, value), + ); + }, + ); +} + +final _projectInputShortcuts = [ + searchShortcut, + undoShortcut, + redoShortcut, + backgroundShortcut, + saveShortcut, + changePathShortcut, + zoomInShortcut, + zoomOutShortcut, + fullScreenShortcut, + hideUIShortcut, + nextPageShortcut, + previousPageShortcut, + selectAllShortcut, + pasteShortcut, + ...changeToolShortcuts, +]; + +Widget _buildHoldShortcutsSection( + BuildContext context, + InputConfiguration config, +) { + return Card( + margin: settingsCardMargin, + child: Padding( + padding: settingsCardPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: settingsCardTitlePadding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context).holdShortcuts, + style: TextTheme.of(context).headlineSmall, + ), + const SizedBox(height: 4), + Text( + AppLocalizations.of(context).holdShortcutsDescription, + style: TextTheme.of(context).bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + IconButton( + icon: const PhosphorIcon(PhosphorIconsLight.plus), + onPressed: () { + context.read().changeInputConfiguration( + config.copyWith( + holdShortcuts: [ + ...config.holdShortcuts, + const HoldShortcut( + keyId: 0, + mapping: InputMapping(InputMapping.handToolValue), + ), + ], + ), + ); + }, + ), + ], + ), + ), + const SizedBox(height: 16), + ...config.holdShortcuts.asMap().entries.map((entry) { + final index = entry.key; + final shortcut = entry.value; + return Row( + children: [ + Expanded( + child: ListTile( + title: Text(shortcut.mapping.getDescription(context)), + subtitle: Text(AppLocalizations.of(context).action), + onTap: () { + openInputMappingModal( + context, + AppLocalizations.of(context).action, + shortcut.mapping, + (mapping) { + final newShortcuts = List.from( + config.holdShortcuts, + ); + newShortcuts[index] = shortcut.copyWith( + mapping: mapping, + ); + context + .read() + .changeInputConfiguration( + config.copyWith(holdShortcuts: newShortcuts), + ); + }, + ); + }, + ), + ), + Expanded( + child: KeyRecorderListTile( + title: Text(AppLocalizations.of(context).key), + currentActivator: SingleActivator( + LogicalKeyboardKey(shortcut.keyId), + ), + onNewKey: (activator) { + final newShortcuts = List.from( + config.holdShortcuts, + ); + newShortcuts[index] = shortcut.copyWith( + keyId: activator.trigger.keyId, + ); + context.read().changeInputConfiguration( + config.copyWith(holdShortcuts: newShortcuts), + ); + }, + ), + ), + IconButton( + icon: const PhosphorIcon(PhosphorIconsLight.trash), + onPressed: () { + final newShortcuts = List.from( + config.holdShortcuts, + ); + newShortcuts.removeAt(index); + context.read().changeInputConfiguration( + config.copyWith(holdShortcuts: newShortcuts), + ); + }, + ), + ], + ); + }), + ], + ), + ), + ); +} + +Widget _buildKeyboardShortcutSection( + BuildContext context, + String title, + List shortcuts, +) { + return ListenableBuilder( + listenable: keybinder, + builder: (context, _) => Card( + margin: settingsCardMargin, + child: Padding( + padding: settingsCardPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: settingsCardTitlePadding, + child: Text(title, style: TextTheme.of(context).headlineSmall), + ), + const SizedBox(height: 16), + LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth; + final columns = width > 600 ? 2 : 1; + final itemWidth = width / columns; + + return Wrap( + children: shortcuts + .map( + (shortcut) => SizedBox( + width: itemWidth, + child: KeyRecorderListTile( + title: Text(shortcut.getLocalizedName(context)), + currentActivator: keybinder.getActivator( + shortcut.id, + ), + onNewKey: (newKey) => + keybinder.updateBinding(shortcut.id, newKey), + onReset: + keybinder.getActivator(shortcut.id) != + shortcut.defaultActivator + ? () => keybinder.resetBinding(shortcut.id) + : null, + ), + ), + ) + .toList(), + ); + }, + ), + ], + ), + ), + ), + ); +} + Widget _selectSensitivitySetting( BuildContext context, ButterflySettings state, diff --git a/app/lib/settings/pages/logs.dart b/app/lib/settings/pages/logs.dart index c70a83b26033..4ac8ad3ee335 100644 --- a/app/lib/settings/pages/logs.dart +++ b/app/lib/settings/pages/logs.dart @@ -9,6 +9,8 @@ final _logsSettingsPage = SettingsLeapPage( settings: [ SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).logs, + descriptionBuilder: (context) => + AppLocalizations.of(context).showVerboseLogsDescription, read: (state) => state.showVerboseLogs, write: (context, value) => context.read().changeShowVerboseLogs(value), diff --git a/app/lib/settings/pages/personalization.dart b/app/lib/settings/pages/personalization.dart index c34a83a5b834..98e3cf67bb05 100644 --- a/app/lib/settings/pages/personalization.dart +++ b/app/lib/settings/pages/personalization.dart @@ -20,29 +20,65 @@ final _personalizationSettingsPage = SettingsLeapPage( ThemeMode.dark => AppLocalizations.of(context).darkTheme, }, ), - SettingsLeapCustomSetting( + SettingsLeapListSetting( + id: 'design', displayName: (context) => AppLocalizations.of(context).design, - builder: (context, state) { - final design = state.design; - return ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.palette), - title: Text(AppLocalizations.of(context).design), - subtitle: Text( - design.isEmpty - ? AppLocalizations.of(context).systemTheme - : design, + descriptionBuilder: (context) => + AppLocalizations.of(context).designDescription, + icon: PhosphorIconsLight.palette, + options: [ + SettingsLeapOption( + id: 'system', + value: '', + displayName: (context) => + AppLocalizations.of(context).systemTheme, + ), + for (final theme in getThemes()) + SettingsLeapOption( + id: theme, + value: theme, + displayName: (context) => theme, ), - trailing: ThemeBox(theme: getThemeData(state.design, false)), - onTap: () => _openDesignModal(context), - ); - }, + ], + read: (state) => state.design, + write: (context, value) => + context.read().changeDesign(value), ), - SettingsLeapCustomSetting( + SettingsLeapListSetting( + id: 'locale', displayName: (context) => AppLocalizations.of(context).locale, - builder: _localeSetting, + descriptionBuilder: (context) => + AppLocalizations.of(context).localeDescription, + icon: PhosphorIconsLight.translate, + options: [ + SettingsLeapOption( + id: 'system', + value: '', + displayName: (context) => + AppLocalizations.of(context).systemLocale, + ), + for (final locale in getLocales()) + SettingsLeapOption( + id: locale.toLanguageTag(), + value: locale.toLanguageTag(), + displayName: (context) => + _localeName(context, locale.toLanguageTag()), + ), + ], + read: (state) => state.localeTag, + write: (context, value) { + final locale = value.isEmpty + ? null + : getLocales() + .where((e) => e.toLanguageTag() == value) + .firstOrNull; + context.read().changeLocale(locale); + }, ), SettingsLeapEnumSetting( displayName: (context) => AppLocalizations.of(context).platformTheme, + descriptionBuilder: (context) => + AppLocalizations.of(context).platformThemeDescription, icon: PhosphorIconsLight.cursor, values: PlatformTheme.values, read: (state) => state.platformTheme, @@ -56,6 +92,8 @@ final _personalizationSettingsPage = SettingsLeapPage( ), SettingsLeapEnumSetting( displayName: (context) => AppLocalizations.of(context).density, + descriptionBuilder: (context) => + AppLocalizations.of(context).densityDescription, icon: PhosphorIconsLight.gridNine, values: ThemeDensity.values, read: (state) => state.density, @@ -76,6 +114,8 @@ final _personalizationSettingsPage = SettingsLeapPage( ), SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).highContrast, + descriptionBuilder: (context) => + AppLocalizations.of(context).highContrastDescription, icon: PhosphorIconsLight.circleHalf, read: (state) => state.highContrast, write: (context, value) => @@ -83,6 +123,8 @@ final _personalizationSettingsPage = SettingsLeapPage( ), SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).nativeTitleBar, + descriptionBuilder: (context) => + AppLocalizations.of(context).nativeTitleBarDescription, icon: PhosphorIconsLight.appWindow, enabled: (context, state) => !kIsWeb && isWindow, read: (state) => state.nativeTitleBar, @@ -97,76 +139,3 @@ final _personalizationSettingsPage = SettingsLeapPage( String _localeName(BuildContext context, String locale) => locale.isNotEmpty ? LocaleNames.of(context)?.nameOf(locale.replaceAll('-', '_')) ?? locale : AppLocalizations.of(context).systemLocale; - -Widget _localeSetting(BuildContext context, ButterflySettings state) { - return ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.translate), - title: Text(AppLocalizations.of(context).locale), - subtitle: Text(_localeName(context, state.localeTag)), - onTap: () => _openLocaleModal(context), - ); -} - -void _openDesignModal(BuildContext context) { - final cubit = context.read(); - final design = cubit.state.design; - showLeapBottomSheet( - context: context, - titleBuilder: (context) => Text(AppLocalizations.of(context).design), - childrenBuilder: (context) { - void changeDesign(String design) { - cubit.changeDesign(design); - Navigator.of(context).pop(); - } - - return [ - ListTile( - title: Text(AppLocalizations.of(context).systemTheme), - selected: design.isEmpty, - onTap: () => changeDesign(''), - leading: ThemeBox(theme: getThemeData('', false)), - ), - ...getThemes().map((e) { - final theme = getThemeData(e, false); - return ListTile( - title: Text(e), - selected: e == design, - onTap: () => changeDesign(e), - leading: ThemeBox(theme: theme), - ); - }), - ]; - }, - ); -} - -void _openLocaleModal(BuildContext context) { - final cubit = context.read(); - final currentLocale = cubit.state.localeTag; - final locales = getLocales(); - showLeapBottomSheet( - context: context, - titleBuilder: (context) => Text(AppLocalizations.of(context).locale), - childrenBuilder: (context) { - void changeLocale(Locale? locale) { - cubit.changeLocale(locale); - Navigator.of(context).pop(); - } - - return [ - ListTile( - title: Text(AppLocalizations.of(context).systemLocale), - selected: currentLocale.isEmpty, - onTap: () => changeLocale(null), - ), - ...locales.map( - (e) => ListTile( - title: Text(_localeName(context, e.toLanguageTag())), - selected: currentLocale == e.toLanguageTag(), - onTap: () => changeLocale(e), - ), - ), - ]; - }, - ); -} diff --git a/app/lib/settings/pages/view.dart b/app/lib/settings/pages/view.dart index 8efcd8fa8772..0d32d61a5bf3 100644 --- a/app/lib/settings/pages/view.dart +++ b/app/lib/settings/pages/view.dart @@ -7,12 +7,33 @@ final _viewSettingsPage = SettingsLeapPage( sections: { 'interface': SettingsLeapSection( settings: [ - SettingsLeapCustomSetting( + SettingsLeapBoolSetting( + id: 'zoomControl', displayName: (context) => AppLocalizations.of(context).zoomControl, - builder: _zoomControlSetting, + descriptionBuilder: (context) => + AppLocalizations.of(context).zoomControlDescription, + icon: PhosphorIconsLight.magnifyingGlass, + read: (state) => state.zoomEnabled, + write: (context, value) => + context.read().changeZoomEnabled(value), + ), + SettingsLeapEnumSetting( + id: 'zoomPosition', + displayName: (context) => AppLocalizations.of(context).zoomPosition, + descriptionBuilder: (context) => + AppLocalizations.of(context).zoomPositionDescription, + icon: PhosphorIconsLight.arrowsOut, + enabled: (context, state) => state.zoomEnabled, + values: ZoomPosition.values, + read: (state) => state.zoomPosition, + write: (context, value) => + context.read().changeZoomPosition(value), + valueLabel: (context, value) => value.getLocalizedName(context), ), SettingsLeapEnumSetting( displayName: (context) => AppLocalizations.of(context).properties, + descriptionBuilder: (context) => + AppLocalizations.of(context).propertiesDescription, icon: PhosphorIconsLight.sliders, values: ZoomPosition.values, read: (state) => state.propertyPosition, @@ -23,6 +44,8 @@ final _viewSettingsPage = SettingsLeapPage( SettingsLeapEnumSetting( displayName: (context) => AppLocalizations.of(context).toolbarPosition, + descriptionBuilder: (context) => + AppLocalizations.of(context).toolbarPositionDescription, icon: PhosphorIconsLight.toolbox, values: ToolbarPosition.values, read: (state) => state.toolbarPosition, @@ -32,6 +55,8 @@ final _viewSettingsPage = SettingsLeapPage( ), SettingsLeapEnumSetting( displayName: (context) => AppLocalizations.of(context).toolbarSize, + descriptionBuilder: (context) => + AppLocalizations.of(context).toolbarSizeDescription, icon: PhosphorIconsLight.toolbox, values: ToolbarSize.values, read: (state) => state.toolbarSize, @@ -41,15 +66,38 @@ final _viewSettingsPage = SettingsLeapPage( ), SettingsLeapCustomSetting( displayName: (context) => AppLocalizations.of(context).toolbarRows, + descriptionBuilder: (context) => + AppLocalizations.of(context).toolbarRowsDescription, builder: _toolbarRowsSetting, ), - SettingsLeapCustomSetting( + SettingsLeapBoolSetting( + id: 'navigationRail', displayName: (context) => AppLocalizations.of(context).navigationRail, - builder: _navigationRailSetting, + descriptionBuilder: (context) => + AppLocalizations.of(context).navigationRailDescription, + icon: PhosphorIconsLight.sidebar, + read: (state) => state.navigationRail, + write: (context, value) => + context.read().changeNavigationRail(value), + ), + SettingsLeapEnumSetting( + id: 'navigatorPosition', + displayName: (context) => AppLocalizations.of(context).position, + descriptionBuilder: (context) => + AppLocalizations.of(context).navigatorPositionDescription, + icon: PhosphorIconsLight.sidebar, + enabled: (context, state) => state.navigationRail, + values: NavigatorPosition.values, + read: (state) => state.navigatorPosition, + write: (context, value) => + context.read().changeNavigatorPosition(value), + valueLabel: _navigatorPositionName, ), SettingsLeapEnumSetting( displayName: (context) => AppLocalizations.of(context).optionsPanelPosition, + descriptionBuilder: (context) => + AppLocalizations.of(context).optionsPanelPositionDescription, icon: PhosphorIconsLight.archive, values: OptionsPanelPosition.values, read: (state) => state.optionsPanelPosition, @@ -60,6 +108,8 @@ final _viewSettingsPage = SettingsLeapPage( SettingsLeapEnumSetting( displayName: (context) => AppLocalizations.of(context).simpleToolbarVisibility, + descriptionBuilder: (context) => + AppLocalizations.of(context).simpleToolbarVisibilityDescription, icon: PhosphorIconsLight.cursorText, values: SimpleToolbarVisibility.values, read: (state) => state.simpleToolbarVisibility, @@ -75,6 +125,8 @@ final _viewSettingsPage = SettingsLeapPage( settings: [ SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).showThumbnails, + descriptionBuilder: (context) => + AppLocalizations.of(context).showThumbnailsDescription, icon: PhosphorIconsLight.image, read: (state) => state.showThumbnails, write: (context, value) => @@ -83,6 +135,8 @@ final _viewSettingsPage = SettingsLeapPage( SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).hideFileExtension, + descriptionBuilder: (context) => + AppLocalizations.of(context).hideFileExtensionDescription, icon: PhosphorIconsLight.fileText, read: (state) => state.hideExtension, write: (context, value) => @@ -93,19 +147,6 @@ final _viewSettingsPage = SettingsLeapPage( }, ); -Widget _contentViewportSetting(BuildContext context, ButterflySettings state) { - return ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.appWindow), - title: Text(AppLocalizations.of(context).contentViewport), - subtitle: Text( - state.limitViewportMultiplier == null - ? AppLocalizations.of(context).off - : '${state.limitViewportMultiplier}x', - ), - onTap: () => _openContentViewportModal(context), - ); -} - Widget _imageScaleSetting(BuildContext context, ButterflySettings state) { return ExactSlider( header: Text(AppLocalizations.of(context).imageScale), @@ -123,93 +164,6 @@ Widget _imageScaleSetting(BuildContext context, ButterflySettings state) { void _openPersistenceSettings(BuildContext context) => context.push('/settings/behaviors/persistence'); -void _openContentViewportModal(BuildContext context) { - final cubit = context.read(); - final currentMultiplier = cubit.state.limitViewportMultiplier; - showLeapBottomSheet( - context: context, - titleBuilder: (context) => - Text(AppLocalizations.of(context).contentViewport), - childrenBuilder: (context) { - final options = [ - (null, AppLocalizations.of(context).off), - (1.0, '1x'), - (1.5, '1.5x'), - (2.0, '2x'), - (3.0, '3x'), - ]; - return options - .map( - (e) => ListTile( - title: Text(e.$2), - selected: currentMultiplier == e.$1, - onTap: () { - cubit.changeLimitViewportMultiplier(e.$1); - Navigator.of(context).pop(); - }, - ), - ) - .toList(); - }, - ); -} - -void _openAutosaveModal(BuildContext context) { - final cubit = context.read(); - final autosave = cubit.state.autosave; - final showSaveButton = cubit.state.showSaveButton; - final delayed = cubit.state.delayedAutosave; - showLeapBottomSheet( - context: context, - titleBuilder: (context) => Text(AppLocalizations.of(context).autosave), - childrenBuilder: (context) { - void changeAutosave(bool? autosave, {bool delayed = false}) { - cubit.changeAutosave(autosave, delayed: delayed); - Navigator.of(context).pop(); - } - - return [ - ListTile( - title: Text(AppLocalizations.of(context).yes), - leading: const Icon(PhosphorIconsLight.check), - selected: autosave && !showSaveButton && !delayed, - onTap: () => changeAutosave(true), - ), - ListTile( - title: Text(AppLocalizations.of(context).delay), - leading: const Icon(PhosphorIconsLight.clock), - selected: autosave && delayed, - onTap: () => changeAutosave(null, delayed: true), - ), - ListTile( - title: Text(AppLocalizations.of(context).yesButShowButtons), - leading: const Icon(PhosphorIconsLight.question), - selected: autosave && showSaveButton && !delayed, - onTap: () => changeAutosave(null), - ), - ListTile( - title: Text(AppLocalizations.of(context).no), - leading: const Icon(PhosphorIconsLight.x), - selected: !autosave, - onTap: () => changeAutosave(false), - ), - ]; - }, - ); -} - -Widget _zoomControlSetting(BuildContext context, ButterflySettings state) { - return AdvancedSwitchListTile( - leading: const PhosphorIcon(PhosphorIconsLight.magnifyingGlass), - title: Text(AppLocalizations.of(context).zoomControl), - subtitle: Text(state.zoomPosition.getLocalizedName(context)), - value: state.zoomEnabled, - onChanged: (value) => - context.read().changeZoomEnabled(value), - onTap: () => _openZoomPositionModal(context), - ); -} - Widget _toolbarRowsSetting(BuildContext context, ButterflySettings state) { return ExactSlider( header: Text(AppLocalizations.of(context).toolbarRows), @@ -226,84 +180,8 @@ Widget _toolbarRowsSetting(BuildContext context, ButterflySettings state) { ); } -Widget _navigationRailSetting(BuildContext context, ButterflySettings state) { - return AdvancedSwitchListTile( - leading: const PhosphorIcon(PhosphorIconsLight.sidebar), - title: Text(AppLocalizations.of(context).navigationRail), - height: 76, - subtitle: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - state.navigatorPosition == NavigatorPosition.left - ? AppLocalizations.of(context).left - : AppLocalizations.of(context).right, - ), - Text( - AppLocalizations.of(context).onlyAvailableLargerScreen, - style: TextTheme.of(context).labelSmall, - ), - ], - ), - onTap: () async { - final position = await showLeapBottomSheet( - context: context, - titleBuilder: (context) => Text(AppLocalizations.of(context).position), - childrenBuilder: (context) => [ - ListTile( - title: Text(AppLocalizations.of(context).left), - selected: state.navigatorPosition == NavigatorPosition.left, - leading: const PhosphorIcon( - PhosphorIconsLight.arrowLineLeft, - textDirection: TextDirection.ltr, - ), - onTap: () => Navigator.of(context).pop(NavigatorPosition.left), - ), - ListTile( - title: Text(AppLocalizations.of(context).right), - selected: state.navigatorPosition == NavigatorPosition.right, - leading: const PhosphorIcon( - PhosphorIconsLight.arrowLineRight, - textDirection: TextDirection.ltr, - ), - onTap: () => Navigator.of(context).pop(NavigatorPosition.right), - ), - ], - ); - if (position != null && context.mounted) { - context.read().changeNavigatorPosition(position); - } - }, - value: state.navigationRail, - onChanged: (value) => - context.read().changeNavigationRail(value), - ); -} - -void _openZoomPositionModal(BuildContext context) { - final cubit = context.read(); - final currentPos = cubit.state.zoomPosition; - showLeapBottomSheet( - context: context, - titleBuilder: (context) => Text(AppLocalizations.of(context).zoomPosition), - childrenBuilder: (context) => ZoomPosition.values - .map( - (e) => ListTile( - title: Text(e.getLocalizedName(context)), - selected: currentPos == e, - leading: Icon(switch (e) { - ZoomPosition.topRight => PhosphorIconsLight.arrowUpRight, - ZoomPosition.topLeft => PhosphorIconsLight.arrowUpLeft, - ZoomPosition.bottomRight => PhosphorIconsLight.arrowDownRight, - ZoomPosition.bottomLeft => PhosphorIconsLight.arrowDownLeft, - }, textDirection: TextDirection.ltr), - onTap: () { - cubit.changeZoomPosition(e); - Navigator.of(context).pop(); - }, - ), - ) - .toList(), - ); -} +String _navigatorPositionName(BuildContext context, NavigatorPosition value) => + switch (value) { + NavigatorPosition.left => AppLocalizations.of(context).left, + NavigatorPosition.right => AppLocalizations.of(context).right, + }; diff --git a/app/lib/widgets/input_mapping_list_tile.dart b/app/lib/widgets/input_mapping_list_tile.dart deleted file mode 100644 index 37eeca87634b..000000000000 --- a/app/lib/widgets/input_mapping_list_tile.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:butterfly/cubits/settings.dart'; -import 'package:butterfly/dialogs/input.dart'; -import 'package:flutter/material.dart'; -import 'package:material_leap/l10n/leap_localizations.dart'; -import 'package:phosphor_flutter/phosphor_flutter.dart'; - -class InputMappingListTile extends StatelessWidget { - final String inputName; - final InputMapping currentValue; - final InputMapping defaultValue; - final Widget icon; - final ValueChanged onChanged; - - const InputMappingListTile({ - super.key, - required this.inputName, - required this.currentValue, - required this.defaultValue, - required this.icon, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(inputName), - leading: icon, - subtitle: Text(currentValue.getDescription(context)), - trailing: currentValue != defaultValue - ? IconButton( - onPressed: () => onChanged(defaultValue), - tooltip: LeapLocalizations.of(context).reset, - icon: const PhosphorIcon(PhosphorIconsLight.clockClockwise), - ) - : null, - onTap: () => - openInputMappingModal(context, inputName, currentValue, onChanged), - ); - } -} diff --git a/app/pubspec.lock b/app/pubspec.lock index 7a524a751d47..f90aab4e034d 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -1274,8 +1274,8 @@ packages: dependency: "direct main" description: path: "packages/settings_leap" - ref: "09c2e7eb9ad1cc82f5a72a92ceb13668b638685a" - resolved-ref: "09c2e7eb9ad1cc82f5a72a92ceb13668b638685a" + ref: "2ecec31542ccafd7a0976f1ba995676b2d8254ba" + resolved-ref: "2ecec31542ccafd7a0976f1ba995676b2d8254ba" url: "https://github.com/LinwoodDev/dart_pkgs.git" source: git version: "0.1.0" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 8c3b252af7c1..4b963680403a 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: settings_leap: git: url: https://github.com/LinwoodDev/dart_pkgs.git - ref: 09c2e7eb9ad1cc82f5a72a92ceb13668b638685a + ref: 2ecec31542ccafd7a0976f1ba995676b2d8254ba path: packages/settings_leap material_leap: git: From 025df87b113882eb117aa78828903aa44957f495 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Wed, 8 Jul 2026 23:46:16 +0200 Subject: [PATCH 047/117] Reorder top corner menu to have home on top, closes #1161 --- app/lib/views/app_bar.dart | 128 +++++++++++++++--------------- app/pubspec.lock | 30 +++---- app/pubspec.yaml | 2 +- metadata/en-US/changelogs/188.txt | 10 +++ 4 files changed, 90 insertions(+), 80 deletions(-) create mode 100644 metadata/en-US/changelogs/188.txt diff --git a/app/lib/views/app_bar.dart b/app/lib/views/app_bar.dart index 854284ba70ef..948a2b0f121e 100644 --- a/app/lib/views/app_bar.dart +++ b/app/lib/views/app_bar.dart @@ -534,37 +534,6 @@ class MainPopupMenu extends StatelessWidget { hideUi != HideState.visible; return MenuAnchor( menuChildren: [ - if (showNavigatorDialog) - ...NavigatorPage.values.map( - (e) => MenuItemButton( - leadingIcon: PhosphorIcon( - e.icon(PhosphorIconsStyle.light), - ), - child: Text(e.getLocalizedName(context)), - onPressed: () { - context.read().setNavigator( - page: e, - ); - final bloc = context.read(); - final transformCubit = context - .read(); - showDialog( - context: context, - builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: bloc), - BlocProvider.value(value: transformCubit), - ], - child: RepositoryProvider.value( - value: cubit, - child: DocumentNavigator(asDialog: true), - ), - ), - ); - }, - ), - ), - if (showNavigatorDialog) const Divider(), if (saveState.embedding == null) ...[ MenuItemButton( leadingIcon: const PhosphorIcon( @@ -733,6 +702,40 @@ class MainPopupMenu extends StatelessWidget { child: Text(AppLocalizations.of(context).packs), ), const Divider(), + ], + if (showNavigatorDialog) ...[ + ...NavigatorPage.values.map( + (e) => MenuItemButton( + leadingIcon: PhosphorIcon( + e.icon(PhosphorIconsStyle.light), + ), + child: Text(e.getLocalizedName(context)), + onPressed: () { + context.read().setNavigator( + page: e, + ); + final bloc = context.read(); + final transformCubit = context + .read(); + showDialog( + context: context, + builder: (context) => MultiBlocProvider( + providers: [ + BlocProvider.value(value: bloc), + BlocProvider.value(value: transformCubit), + ], + child: RepositoryProvider.value( + value: cubit, + child: DocumentNavigator(asDialog: true), + ), + ), + ); + }, + ), + ), + const Divider(), + ], + if (saveState.embedding == null) MenuItemButton( leadingIcon: const PhosphorIcon( PhosphorIconsLight.filePlus, @@ -752,41 +755,38 @@ class MainPopupMenu extends StatelessWidget { AppLocalizations.of(context).newContent, ), ), - MenuItemButton( - leadingIcon: const PhosphorIcon( - PhosphorIconsLight.file, - textDirection: TextDirection.ltr, - ), - shortcut: const SingleActivator( - LogicalKeyboardKey.keyN, - shift: true, - control: true, - ), - onPressed: () { - Actions.maybeInvoke( - context, - NewIntent(fromTemplate: true), - ); - }, - child: Text(AppLocalizations.of(context).templates), + MenuItemButton( + leadingIcon: const PhosphorIcon( + PhosphorIconsLight.file, + textDirection: TextDirection.ltr, ), - SubmenuButton( - menuChildren: settings.history - .map( - (e) => MenuItemButton( - child: Text(e.identifier), - onPressed: () => openFile(context, true, e), - ), - ) - .toList(), - leadingIcon: const PhosphorIcon( - PhosphorIconsLight.clock, - ), - child: Text( - AppLocalizations.of(context).recentFiles, - ), + shortcut: const SingleActivator( + LogicalKeyboardKey.keyN, + shift: true, + control: true, ), - ], + onPressed: () { + Actions.maybeInvoke( + context, + NewIntent(fromTemplate: true), + ); + }, + child: Text(AppLocalizations.of(context).templates), + ), + SubmenuButton( + menuChildren: settings.history + .map( + (e) => MenuItemButton( + child: Text(e.identifier), + onPressed: () => openFile(context, true, e), + ), + ) + .toList(), + leadingIcon: const PhosphorIcon( + PhosphorIconsLight.clock, + ), + child: Text(AppLocalizations.of(context).recentFiles), + ), if (saveState.embedding == null) ...[ MenuItemButton( leadingIcon: const PhosphorIcon( diff --git a/app/pubspec.lock b/app/pubspec.lock index f90aab4e034d..9393b82a82ca 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: build - sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10 + sha256: "45d14a0fb23e018d8287c32fc98d726ce466b231928ed9b9200f29bd3ccd39ae" url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "4.0.7" build_cli_annotations: dependency: transitive description: @@ -109,26 +109,26 @@ packages: dependency: transitive description: name: build_config - sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" + sha256: f2c223156a26eea323e6244b85141d76413a80aeee9fe0b380773789fabaf8ae url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" build_daemon: dependency: transitive description: name: build_daemon - sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + sha256: fd754058c342243718d5171a95f352cfc9fcf0cba8cfa26df67cb13a5836db78 url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6" + sha256: "5367e521935b102bdf1e735d2aab461e36b2edca6517662d088dd04cc39f8d16" url: "https://pub.dev" source: hosted - version: "2.15.0" + version: "2.15.1" built_collection: dependency: transitive description: @@ -164,18 +164,18 @@ packages: dependency: transitive description: name: camera_android_camerax - sha256: "50c3fd81228826635af4875330febb193c74a2e12a4d19c1b95e2a67f0017bb3" + sha256: cf6c248ef3d3c4846e99e488de9487d95927437e083c396e5fed2c1ece7f93a7 url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4+1" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "90e4cc3fde331581a3b2d35d83be41dbb7393af0ab857eb27b732174289cb96d" + sha256: "866e9cd8370f8055d005c0413937a52dc1d7a472687f0ee3ce02392955aababa" url: "https://pub.dev" source: hosted - version: "0.10.1" + version: "0.10.2" camera_platform_interface: dependency: transitive description: @@ -667,10 +667,10 @@ packages: dependency: "direct main" description: name: idb_shim - sha256: dcf59807be0cdf39f305c997b7b29cb06fdef26311559934c82cbc3e8c241f1c + sha256: "3448298f244bc76a14aca71461eeab6add2b8a877930521c42c2e8b71b644590" url: "https://pub.dev" source: hosted - version: "2.9.6+1" + version: "2.9.6+2" image: dependency: "direct main" description: @@ -1727,4 +1727,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.12.2 <4.0.0" - flutter: "3.44.4" + flutter: "3.44.5" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 4b963680403a..bf35cda23afd 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -17,7 +17,7 @@ version: 2.6.0-beta.2+188 environment: sdk: ">=3.12.2 <4.0.0" - flutter: 3.44.4 + flutter: 3.44.5 dependencies: flutter: diff --git a/metadata/en-US/changelogs/188.txt b/metadata/en-US/changelogs/188.txt new file mode 100644 index 000000000000..966043e501b1 --- /dev/null +++ b/metadata/en-US/changelogs/188.txt @@ -0,0 +1,10 @@ +* Add persistent document states ([#1077](https://github.com/LinwoodDev/Butterfly/issues/1077)) +* Reorder top corner menu to have home on top ([#1161](https://github.com/LinwoodDev/Butterfly/issues/1161)) +* Rebuild internal settings pages + * Add search bar to settings pages ([#1158](https://github.com/LinwoodDev/Butterfly/issues/1158)) + * Always have settings value on the right side + * Add settings descriptions +* Refactor whole state management structure ([#1157](https://github.com/LinwoodDev/Butterfly/pull/1157)) +* Remove unused view options + +Read more here: https://linwood.dev/butterfly/2.6.0-beta.2 \ No newline at end of file From d7af30e5d7f2f313a91d2f44b98a8250e77c27b2 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Wed, 8 Jul 2026 23:52:39 +0200 Subject: [PATCH 048/117] Fix wrong title bar on wayland on non gnome desktops --- app/linux/runner/my_application.cc | 18 +++++++++++++----- metadata/en-US/changelogs/188.txt | 1 + 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/linux/runner/my_application.cc b/app/linux/runner/my_application.cc index 07506e5a15da..8dd836d2ff91 100644 --- a/app/linux/runner/my_application.cc +++ b/app/linux/runner/my_application.cc @@ -33,16 +33,24 @@ static void my_application_activate(GApplication* application) { // in case the window manager does more exotic layout, e.g. tiling. // If running on Wayland assume the header bar will work (may need changing // if future cases occur). - gboolean use_header_bar = TRUE; + gboolean use_header_bar = FALSE; #ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); + GdkScreen* screen = gtk_window_get_screen(GTK_WINDOW(window)); if (GDK_IS_X11_SCREEN(screen)) { const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { - use_header_bar = FALSE; + if (g_strcmp0(wm_name, "GNOME Shell") == 0) { + use_header_bar = TRUE; } - } + } else #endif + { + const gchar* current_desktop = g_getenv("XDG_CURRENT_DESKTOP"); + if (current_desktop != nullptr) { + g_auto(GStrv) desktops = g_strsplit(current_desktop, ":", -1); + use_header_bar = g_strv_contains( + reinterpret_cast(desktops), "GNOME"); + } + } if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); diff --git a/metadata/en-US/changelogs/188.txt b/metadata/en-US/changelogs/188.txt index 966043e501b1..23c1bda6763d 100644 --- a/metadata/en-US/changelogs/188.txt +++ b/metadata/en-US/changelogs/188.txt @@ -6,5 +6,6 @@ * Add settings descriptions * Refactor whole state management structure ([#1157](https://github.com/LinwoodDev/Butterfly/pull/1157)) * Remove unused view options +* Fix wrong title bar on wayland on non gnome desktops Read more here: https://linwood.dev/butterfly/2.6.0-beta.2 \ No newline at end of file From 55c7d2d23b443deede0b948eeb5d7376cbbcc05c Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Fri, 10 Jul 2026 21:28:36 +0200 Subject: [PATCH 049/117] Revert "Fix wrong title bar on wayland on non gnome desktops" This reverts commit d7af30e5d7f2f313a91d2f44b98a8250e77c27b2. Sadly this breaks custom title bars so we need to revert it. Maybe we find a better option --- app/linux/runner/my_application.cc | 18 +++++------------- metadata/en-US/changelogs/188.txt | 1 - 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/app/linux/runner/my_application.cc b/app/linux/runner/my_application.cc index 8dd836d2ff91..07506e5a15da 100644 --- a/app/linux/runner/my_application.cc +++ b/app/linux/runner/my_application.cc @@ -33,24 +33,16 @@ static void my_application_activate(GApplication* application) { // in case the window manager does more exotic layout, e.g. tiling. // If running on Wayland assume the header bar will work (may need changing // if future cases occur). - gboolean use_header_bar = FALSE; + gboolean use_header_bar = TRUE; #ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(GTK_WINDOW(window)); + GdkScreen* screen = gtk_window_get_screen(window); if (GDK_IS_X11_SCREEN(screen)) { const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") == 0) { - use_header_bar = TRUE; - } - } else -#endif - { - const gchar* current_desktop = g_getenv("XDG_CURRENT_DESKTOP"); - if (current_desktop != nullptr) { - g_auto(GStrv) desktops = g_strsplit(current_desktop, ":", -1); - use_header_bar = g_strv_contains( - reinterpret_cast(desktops), "GNOME"); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; } } +#endif if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); diff --git a/metadata/en-US/changelogs/188.txt b/metadata/en-US/changelogs/188.txt index 23c1bda6763d..966043e501b1 100644 --- a/metadata/en-US/changelogs/188.txt +++ b/metadata/en-US/changelogs/188.txt @@ -6,6 +6,5 @@ * Add settings descriptions * Refactor whole state management structure ([#1157](https://github.com/LinwoodDev/Butterfly/pull/1157)) * Remove unused view options -* Fix wrong title bar on wayland on non gnome desktops Read more here: https://linwood.dev/butterfly/2.6.0-beta.2 \ No newline at end of file From 57fa5af91354e79bd1a54519016197212ea23d1c Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sat, 11 Jul 2026 16:43:02 +0200 Subject: [PATCH 050/117] Fix crash with android saf on folders with many files --- .../dev/linwood/butterfly/MainActivity.java | 1 + app/android/gradle.properties | 8 +++++ .../gradle/wrapper/gradle-wrapper.properties | 3 +- app/android/settings.gradle.kts | 2 +- .../Flutter/GeneratedPluginRegistrant.swift | 2 ++ app/pubspec.lock | 30 +++++++++---------- app/pubspec.yaml | 4 +-- metadata/en-US/changelogs/188.txt | 2 ++ 8 files changed, 32 insertions(+), 20 deletions(-) diff --git a/app/android/app/src/main/java/dev/linwood/butterfly/MainActivity.java b/app/android/app/src/main/java/dev/linwood/butterfly/MainActivity.java index 7d61289ff61d..b6efe6407c57 100644 --- a/app/android/app/src/main/java/dev/linwood/butterfly/MainActivity.java +++ b/app/android/app/src/main/java/dev/linwood/butterfly/MainActivity.java @@ -65,6 +65,7 @@ private boolean handleIntent(Intent intent) { return true; } } catch (IOException e) { + //noinspection CallToPrintStackTrace e.printStackTrace(); intentData = null; intentType = null; diff --git a/app/android/gradle.properties b/app/android/gradle.properties index 229a7e4e9289..5e6a5f865505 100644 --- a/app/android/gradle.properties +++ b/app/android/gradle.properties @@ -7,3 +7,11 @@ android.nonFinalResIds=false android.builtInKotlin=false # This newDsl flag was added automatically by Flutter migrator android.newDsl=false +android.defaults.buildfeatures.resvalues=true +android.sdk.defaultTargetSdkToCompileSdkIfUnset=false +android.enableAppCompileTimeRClass=false +android.usesSdkInManifest.disallowed=false +android.uniquePackageNames=false +android.dependency.useConstraints=true +android.r8.strictFullModeForKeepRules=false +android.r8.optimizedResourceShrinking=false diff --git a/app/android/gradle/wrapper/gradle-wrapper.properties b/app/android/gradle/wrapper/gradle-wrapper.properties index a4cf193f6381..c61a118f7ddb 100644 --- a/app/android/gradle/wrapper/gradle-wrapper.properties +++ b/app/android/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=6f74b601422d6d6fc4e1f9a1ab6522f642c2fdcbc15ae33ebd30ba3d7198e854 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/app/android/settings.gradle.kts b/app/android/settings.gradle.kts index 97397bb41b5a..2c69157c7661 100644 --- a/app/android/settings.gradle.kts +++ b/app/android/settings.gradle.kts @@ -18,7 +18,7 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.13.2" apply false + id("com.android.application") version "9.2.1" apply false id("org.jetbrains.kotlin.android") version "2.3.21" apply false } diff --git a/app/macos/Flutter/GeneratedPluginRegistrant.swift b/app/macos/Flutter/GeneratedPluginRegistrant.swift index ac5eaac51c86..2e94b8755e42 100644 --- a/app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -13,6 +13,7 @@ import flutter_secure_storage_darwin import lw_sysapi import network_info_plus import package_info_plus +import pdfium_flutter import screen_retriever_macos import share_plus import shared_preferences_foundation @@ -28,6 +29,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SwiftLwSysapiPlugin.register(with: registry.registrar(forPlugin: "SwiftLwSysapiPlugin")) NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PDFiumFlutterPlugin.register(with: registry.registrar(forPlugin: "PDFiumFlutterPlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/app/pubspec.lock b/app/pubspec.lock index 9393b82a82ca..155a809f257e 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -180,10 +180,10 @@ packages: dependency: transitive description: name: camera_platform_interface - sha256: "7ac852d77699acee79f0d438b793feee26721841e50973576419ff5c6d95e9b7" + sha256: "4524ca6eb4176b066864036ad4fe02c3e4863e63b77eadc21a5bf56824f43498" url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.13.1" camera_web: dependency: transitive description: @@ -484,18 +484,18 @@ packages: dependency: transitive description: name: flutter_rust_bridge - sha256: "20861a4148ebb72f547366b2dd575ef803e32ff1cb6dc5aa0c9ffdbc774f2799" + sha256: d9d819e3f39efe08a68309ff7cadc9d8bcfd65d86319e0c5735cd4f4fee16c4a url: "https://pub.dev" source: hosted - version: "2.13.0-beta.4" + version: "2.13.0-beta.5" flutter_rust_bridge_hooks: dependency: transitive description: name: flutter_rust_bridge_hooks - sha256: "0ae752d541b6878ac1baac7827502ea21cb3ac81bf57362895a6df2929707ac7" + sha256: "33f96e930af9016405f519696574f6fcb96ec37a515188d1da484fa81f9a90f7" url: "https://pub.dev" source: hosted - version: "2.13.0-beta.4" + version: "2.13.0-beta.5" flutter_secure_storage: dependency: "direct main" description: @@ -785,8 +785,8 @@ packages: dependency: "direct main" description: path: "packages/lw_file_system" - ref: "064aed61c361194bf0eb3cb599e62bdd3c111d7c" - resolved-ref: "064aed61c361194bf0eb3cb599e62bdd3c111d7c" + ref: "18711cf5297f9679701536255dfe3d650b72fd25" + resolved-ref: "18711cf5297f9679701536255dfe3d650b72fd25" url: "https://github.com/LinwoodDev/dart_pkgs.git" source: git version: "1.0.0" @@ -1065,26 +1065,26 @@ packages: dependency: transitive description: name: pdfium_flutter - sha256: "420ba8e7673b54da387ceeeb18a72c8bc6e4452128dbb391dca900c748f9e9ba" + sha256: "0b115c0917aef9cf6bb9c4d47d0efc61d97f44844cd76287c921f997c26cf15d" url: "https://pub.dev" source: hosted - version: "0.2.2" + version: "0.2.3" pdfrx: dependency: "direct main" description: name: pdfrx - sha256: e0ca318004c3f32144db8e74fa612abb80ebe79004677293af74ed9af119f47f + sha256: acce61944c31bd36f0c3031b1bbf41f59d0b0d4e5e27614fbdc74ff093d55464 url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.7" pdfrx_engine: dependency: transitive description: name: pdfrx_engine - sha256: "89865e158ced818690ab207a13a34a65803cd67ee1bec9f5f87838eb5a79600b" + sha256: ef8f8cfa64255bfc91a559dd2d32d7cfe7a0f8003bac297bbf9a0aed1182fae5 url: "https://pub.dev" source: hosted - version: "0.4.3" + version: "0.4.6" perfect_freehand: dependency: "direct main" description: @@ -1727,4 +1727,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.12.2 <4.0.0" - flutter: "3.44.5" + flutter: "3.44.6" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index bf35cda23afd..348eaeb65d1a 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -17,7 +17,7 @@ version: 2.6.0-beta.2+188 environment: sdk: ">=3.12.2 <4.0.0" - flutter: 3.44.5 + flutter: 3.44.6 dependencies: flutter: @@ -98,7 +98,7 @@ dependencies: lw_file_system: git: url: https://github.com/LinwoodDev/dart_pkgs.git - ref: 064aed61c361194bf0eb3cb599e62bdd3c111d7c + ref: 18711cf5297f9679701536255dfe3d650b72fd25 path: packages/lw_file_system keybinder: git: diff --git a/metadata/en-US/changelogs/188.txt b/metadata/en-US/changelogs/188.txt index 966043e501b1..3d444e4f6e64 100644 --- a/metadata/en-US/changelogs/188.txt +++ b/metadata/en-US/changelogs/188.txt @@ -6,5 +6,7 @@ * Add settings descriptions * Refactor whole state management structure ([#1157](https://github.com/LinwoodDev/Butterfly/pull/1157)) * Remove unused view options +* Fix crash with android saf on folders with many files +* Upgrade to agb 9 Read more here: https://linwood.dev/butterfly/2.6.0-beta.2 \ No newline at end of file From dba94f42c4c43be31e4201356a1491916934eeea Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sat, 11 Jul 2026 17:23:00 +0200 Subject: [PATCH 051/117] Add better settings description --- app/lib/l10n/app_en.arb | 95 +++++++++------------ app/lib/settings/pages/behaviors/home.dart | 20 ++--- app/lib/settings/pages/data.dart | 2 - app/lib/settings/pages/experiments.dart | 6 +- app/lib/settings/pages/inputs.dart | 22 ++--- app/lib/settings/pages/logs.dart | 16 ++-- app/lib/settings/pages/personalization.dart | 12 ++- app/lib/settings/pages/view.dart | 69 ++++++--------- app/pubspec.lock | 8 +- app/pubspec.yaml | 4 +- 10 files changed, 98 insertions(+), 156 deletions(-) diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index a61ea3900780..11bbaa2adb79 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -9,7 +9,6 @@ "darkTheme": "Dark theme", "lightTheme": "Light theme", "systemTheme": "Use default system theme", - "designDescription": "Choose the visual theme design", "view": "View", "contentViewport": "Content Viewport", "@contentViewport": { @@ -115,7 +114,6 @@ }, "locale": "Locale", "systemLocale": "System locale", - "localeDescription": "Choose the app language", "information": "Information", "license": "License", "imprint": "Imprint", @@ -324,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Sensitivity", - "sensitivityHint": "The higher the value, the more sensitive the input", "horizontal": "Horizontal", "vertical": "Vertical", "plain": "Plain", @@ -647,29 +644,23 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Input gestures", - "inputGesturesDescription": "Enable touch gestures for moving and zooming the canvas", - "gestureSensitivityDescription": "Adjust how fast two-finger pan and zoom gestures move the canvas", - "touchSensitivityDescription": "Increase this to make touch targets easier to hit", - "selectSensitivityDescription": "Increase this to make element selection easier", - "scrollSensitivityDescription": "Adjust how fast mouse wheel scrolling moves or zooms the canvas", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Native title bar", - "nativeTitleBarDescription": "Use the operating system title bar on desktop", "mode": "Mode", "syncMode": "Sync mode", - "syncModeDescription": "Choose when remote files sync", "connection": "Connection", "always": "Always", "@always": { "description": "Frequency" }, "noMobile": "No mobile", - "syncModeAlwaysDescription": "Sync automatically whenever files change", - "syncModeNoMobileDescription": "Sync automatically except on mobile devices", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Manual", "@manual": { "description": "Manual mode" }, - "syncModeManualDescription": "Only sync when you trigger it manually", + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Search", "@search": { "description": "Search action" @@ -996,7 +987,6 @@ "ascending": "Ascending", "descending": "Descending", "imageScale": "Image scale", - "imageScaleDescription": "Scale imported images when their size is detected automatically", "svgScale": "SVG scale", "noImageSelected": "No image selected", "noSvgSelected": "No SVG selected", @@ -1368,9 +1358,9 @@ "@ignorePressure": { "description": "Choose how stylus pressure is handled" }, - "ignoreFirstPressureDescription": "On some devices, the first pressure value is not accurate. This setting will ignore the first pressure value and use the pressure of the second event instead.", - "ignorePressureNeverDescription": "Use every pressure value from the device", - "ignorePressureAlwaysDescription": "Ignore pressure and treat the pen as fully pressed", + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Temporary", "simpleToolbarVisibility": "Simple toolbar visibility", "@simpleToolbarVisibility": { @@ -1468,37 +1458,20 @@ } } }, - "zoomControlDescription": "Show zoom controls in the canvas view", - "zoomPositionDescription": "Choose where the zoom controls appear", - "propertiesDescription": "Choose where the properties panel appears", - "toolbarPositionDescription": "Choose where the main toolbar appears", - "toolbarSizeDescription": "Choose how large the toolbar buttons are", - "toolbarRowsDescription": "Set how many rows the toolbar can use", - "navigationRailDescription": "Show a navigation rail on wider screens", - "navigatorPositionDescription": "Choose which side the navigator appears on", - "optionsPanelPositionDescription": "Choose whether the options panel appears above or below the canvas", - "simpleToolbarVisibilityDescription": "Choose when the simplified toolbar is shown", - "showThumbnailsDescription": "Show thumbnails for notes in the file list", - "hideFileExtensionDescription": "Hide file extensions in file names", - "highContrastDescription": "Use stronger contrast in the app theme", - "autosaveDescription": "Choose how changes are saved", - "autosaveEnabledDescription": "Save changes automatically as you work", - "autosaveDelayedDescription": "Save changes automatically after a short delay", - "autosaveShowButtonDescription": "Show a save button and save manually", - "autosaveDisabledDescription": "Turn off automatic saving", - "autosaveDelayDescription": "Wait time before delayed autosave runs", - "hideCursorWhileDrawingDescription": "Hide the mouse cursor while drawing", - "onStartupDescription": "Choose what opens when the app starts", - "onStartupHomeScreenDescription": "Open the home screen when the app starts", - "onStartupLastNoteDescription": "Reopen the most recent note when the app starts", - "onStartupNewNoteDescription": "Create a new note when the app starts", - "smoothNavigationDescription": "Reduce the amount of rendering work while navigating", - "edgePanAreaSwitchingDescription": "Switch areas when you pan near the edge of the canvas", - "collaborationDescription": "Allow multiple people to edit the same note together", - "densityDescription": "Choose how compact the interface should be", - "showVerboseLogsDescription": "Include debug and verbose logs in the log view and console", - "bringMovedElementsToFrontDescription": "Move dragged elements in front of other elements", - "persistenceDocumentStatesDescription": "Choose which document state details are stored between sessions", + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", "persistentStatesEnabledDescription": "Store document state information between app sessions", "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", "persistentStateViewportDescription": "Remember the viewport position and zoom", @@ -1509,13 +1482,21 @@ "persistentStateAreasDescription": "Remember the area panel state when reopening a file", "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", - "contentViewportDescription": "Limit how far the viewport can move beyond the content", - "limitViewportPositiveDescription": "Prevent the viewport from moving into negative coordinates", - "startInFullScreenDescription": "Open the app in full screen", - "platformThemeDescription": "Choose whether the app follows the system, desktop, or mobile layout", - "penOnlyInputDescription": "Choose when the app should ignore touch and mouse input", - "showPenOnlyToggleDescription": "Show the pen only toggle button when a stylus is detected", - "ignorePressureDescription": "Choose how stylus pressure is handled", - "moveOnGestureDescription": "Move the canvas when multi-touch gestures are used", - "spreadPagesDescription": "Split imported content across multiple pages" + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } diff --git a/app/lib/settings/pages/behaviors/home.dart b/app/lib/settings/pages/behaviors/home.dart index fd0e6c8d84e6..ef5d16b43872 100644 --- a/app/lib/settings/pages/behaviors/home.dart +++ b/app/lib/settings/pages/behaviors/home.dart @@ -11,8 +11,6 @@ final _behaviorsSettingsPage = SettingsLeapPage( SettingsLeapListSetting( id: 'autosave', displayName: (context) => AppLocalizations.of(context).autosave, - descriptionBuilder: (context) => - AppLocalizations.of(context).autosaveDescription, icon: PhosphorIconsLight.floppyDisk, keywordsBuilder: (context) => [AppLocalizations.of(context).save], options: [ @@ -60,15 +58,11 @@ final _behaviorsSettingsPage = SettingsLeapPage( ), SettingsLeapCustomSetting( displayName: (context) => AppLocalizations.of(context).autosaveDelay, - descriptionBuilder: (context) => - AppLocalizations.of(context).autosaveDelayDescription, enabled: (context, state) => state.autosave && state.delayedAutosave, builder: _autosaveDelaySetting, ), SettingsLeapEnumSetting( displayName: (context) => AppLocalizations.of(context).onStartup, - descriptionBuilder: (context) => - AppLocalizations.of(context).onStartupDescription, icon: PhosphorIconsLight.arrowFatLineUp, values: StartupBehavior.values, read: (state) => state.onStartup, @@ -90,7 +84,7 @@ final _behaviorsSettingsPage = SettingsLeapPage( SettingsLeapActionSetting( displayName: (context) => AppLocalizations.of(context).persistenceDocumentStates, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).persistenceDocumentStatesDescription, icon: PhosphorIconsLight.database, onTap: _openPersistenceSettings, @@ -98,7 +92,7 @@ final _behaviorsSettingsPage = SettingsLeapPage( SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).startInFullScreen, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).startInFullScreenDescription, icon: PhosphorIconsLight.arrowsOut, read: (state) => state.startInFullScreen, @@ -109,7 +103,7 @@ final _behaviorsSettingsPage = SettingsLeapPage( id: 'contentViewport', displayName: (context) => AppLocalizations.of(context).contentViewport, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).contentViewportDescription, icon: PhosphorIconsLight.appWindow, options: [ @@ -147,7 +141,7 @@ final _behaviorsSettingsPage = SettingsLeapPage( SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).limitViewportToPositiveCoordinates, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).limitViewportPositiveDescription, icon: PhosphorIconsLight.plusSquare, read: (state) => state.limitViewportPositive, @@ -178,7 +172,7 @@ final _behaviorsSettingsPage = SettingsLeapPage( SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).bringMovedElementsToFront, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).bringMovedElementsToFrontDescription, icon: PhosphorIconsLight.stack, read: (state) => state.bringMovedElementsToFront, @@ -193,7 +187,7 @@ final _behaviorsSettingsPage = SettingsLeapPage( settings: [ SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).spreadToPages, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).spreadPagesDescription, icon: PhosphorIconsLight.arrowsOutSimple, read: (state) => state.spreadPages, @@ -202,8 +196,6 @@ final _behaviorsSettingsPage = SettingsLeapPage( ), SettingsLeapCustomSetting( displayName: (context) => AppLocalizations.of(context).imageScale, - descriptionBuilder: (context) => - AppLocalizations.of(context).imageScaleDescription, builder: _imageScaleSetting, ), ], diff --git a/app/lib/settings/pages/data.dart b/app/lib/settings/pages/data.dart index 3a0d6f64c213..a19da56b180a 100644 --- a/app/lib/settings/pages/data.dart +++ b/app/lib/settings/pages/data.dart @@ -9,8 +9,6 @@ final _dataSettingsPage = SettingsLeapPage( settings: [ SettingsLeapEnumSetting( displayName: (context) => AppLocalizations.of(context).syncMode, - descriptionBuilder: (context) => - AppLocalizations.of(context).syncModeDescription, icon: PhosphorIconsLight.cloudArrowDown, enabled: (context, state) => !kIsWeb, values: SyncMode.values, diff --git a/app/lib/settings/pages/experiments.dart b/app/lib/settings/pages/experiments.dart index 779bdb4b4c56..37dfd669b8d2 100644 --- a/app/lib/settings/pages/experiments.dart +++ b/app/lib/settings/pages/experiments.dart @@ -16,7 +16,7 @@ final _experimentsSettingsPage = SettingsLeapPage( settings: [ SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).collaboration, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).collaborationDescription, icon: PhosphorIconsLight.chatTeardrop, read: (state) => state.hasFlag('collaboration'), @@ -26,7 +26,7 @@ final _experimentsSettingsPage = SettingsLeapPage( SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).smoothNavigation, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).smoothNavigationDescription, icon: PhosphorIconsLight.caretCircleDoubleDown, read: (state) => state.hasFlag('smoothNavigation'), @@ -36,7 +36,7 @@ final _experimentsSettingsPage = SettingsLeapPage( SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).edgePanAreaSwitching, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).edgePanAreaSwitchingDescription, icon: PhosphorIconsLight.cursor, read: (state) => state.hasFlag('edgePanAreaSwitching'), diff --git a/app/lib/settings/pages/inputs.dart b/app/lib/settings/pages/inputs.dart index d485d2619fe8..1aedc48aac35 100644 --- a/app/lib/settings/pages/inputs.dart +++ b/app/lib/settings/pages/inputs.dart @@ -41,31 +41,21 @@ final _inputsSettingsPage = SettingsLeapPage( ), 'sensitivity': SettingsLeapSection( displayName: (context) => AppLocalizations.of(context).sensitivity, - descriptionBuilder: (context) => - AppLocalizations.of(context).sensitivityHint, settings: [ SettingsLeapCustomSetting( displayName: (context) => AppLocalizations.of(context).select, - descriptionBuilder: (context) => - AppLocalizations.of(context).selectSensitivityDescription, builder: _selectSensitivitySetting, ), SettingsLeapCustomSetting( displayName: (context) => AppLocalizations.of(context).touch, - descriptionBuilder: (context) => - AppLocalizations.of(context).touchSensitivityDescription, builder: _touchSensitivitySetting, ), SettingsLeapCustomSetting( displayName: (context) => AppLocalizations.of(context).inputGestures, - descriptionBuilder: (context) => - AppLocalizations.of(context).gestureSensitivityDescription, builder: _gestureSensitivitySetting, ), SettingsLeapCustomSetting( displayName: (context) => AppLocalizations.of(context).scroll, - descriptionBuilder: (context) => - AppLocalizations.of(context).scrollSensitivityDescription, builder: _scrollSensitivitySetting, ), ], @@ -88,7 +78,7 @@ final _mouseSettingsPage = SettingsLeapPage( id: 'hideCursorWhileDrawing', displayName: (context) => AppLocalizations.of(context).hideCursorWhileDrawing, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).hideCursorWhileDrawingDescription, icon: PhosphorIconsLight.cursorClick, read: (state) => state.hideCursorWhileDrawing, @@ -191,7 +181,7 @@ final _touchSettingsPage = SettingsLeapPage( SettingsLeapBoolSetting( id: 'inputGestures', displayName: (context) => AppLocalizations.of(context).inputGestures, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).inputGesturesDescription, icon: PhosphorIconsLight.handTap, read: (state) => state.inputGestures, @@ -201,7 +191,7 @@ final _touchSettingsPage = SettingsLeapPage( SettingsLeapBoolSetting( id: 'moveOnGesture', displayName: (context) => AppLocalizations.of(context).moveOnGesture, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).moveOnGestureDescription, icon: PhosphorIconsLight.arrowsOutCardinal, read: (state) => state.moveOnGesture, @@ -318,7 +308,7 @@ final _penSettingsPage = SettingsLeapPage( SettingsLeapListSetting( id: 'penOnlyInput', displayName: (context) => AppLocalizations.of(context).penOnlyInput, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).penOnlyInputDescription, icon: PhosphorIconsLight.pencilSimpleLine, options: [ @@ -352,7 +342,7 @@ final _penSettingsPage = SettingsLeapPage( id: 'showPenOnlyToggle', displayName: (context) => AppLocalizations.of(context).showPenOnlyToggle, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).showPenOnlyToggleDescription, icon: PhosphorIconsLight.toggleRight, read: (state) => state.showPenOnlyToggle, @@ -362,7 +352,7 @@ final _penSettingsPage = SettingsLeapPage( SettingsLeapListSetting( id: 'ignorePressure', displayName: (context) => AppLocalizations.of(context).ignorePressure, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).ignorePressureDescription, icon: PhosphorIconsLight.lineSegments, options: [ diff --git a/app/lib/settings/pages/logs.dart b/app/lib/settings/pages/logs.dart index 4ac8ad3ee335..839cf77ad5bc 100644 --- a/app/lib/settings/pages/logs.dart +++ b/app/lib/settings/pages/logs.dart @@ -9,7 +9,7 @@ final _logsSettingsPage = SettingsLeapPage( settings: [ SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).logs, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).showVerboseLogsDescription, read: (state) => state.showVerboseLogs, write: (context, value) => @@ -27,7 +27,12 @@ Widget _logsSection( BuildContext context, ButterflySettings state, Widget child, -) => const LogsSettingsContent(); +) => Column( + children: [ + child, + const Expanded(child: LogsSettingsContent()), + ], +); class LogsSettingsContent extends StatefulWidget { const LogsSettingsContent({super.key}); @@ -167,13 +172,6 @@ class _LogsSettingsContentState extends State { ), ], ), - SwitchListTile( - title: const Text('Show verbose logs'), - value: state.showVerboseLogs, - onChanged: (value) { - context.read().changeShowVerboseLogs(value); - }, - ), Expanded( child: _selectedFile == null ? StreamBuilder( diff --git a/app/lib/settings/pages/personalization.dart b/app/lib/settings/pages/personalization.dart index 98e3cf67bb05..9994c3c7f29f 100644 --- a/app/lib/settings/pages/personalization.dart +++ b/app/lib/settings/pages/personalization.dart @@ -23,7 +23,7 @@ final _personalizationSettingsPage = SettingsLeapPage( SettingsLeapListSetting( id: 'design', displayName: (context) => AppLocalizations.of(context).design, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).designDescription, icon: PhosphorIconsLight.palette, options: [ @@ -47,8 +47,6 @@ final _personalizationSettingsPage = SettingsLeapPage( SettingsLeapListSetting( id: 'locale', displayName: (context) => AppLocalizations.of(context).locale, - descriptionBuilder: (context) => - AppLocalizations.of(context).localeDescription, icon: PhosphorIconsLight.translate, options: [ SettingsLeapOption( @@ -77,7 +75,7 @@ final _personalizationSettingsPage = SettingsLeapPage( ), SettingsLeapEnumSetting( displayName: (context) => AppLocalizations.of(context).platformTheme, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).platformThemeDescription, icon: PhosphorIconsLight.cursor, values: PlatformTheme.values, @@ -92,7 +90,7 @@ final _personalizationSettingsPage = SettingsLeapPage( ), SettingsLeapEnumSetting( displayName: (context) => AppLocalizations.of(context).density, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).densityDescription, icon: PhosphorIconsLight.gridNine, values: ThemeDensity.values, @@ -114,7 +112,7 @@ final _personalizationSettingsPage = SettingsLeapPage( ), SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).highContrast, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).highContrastDescription, icon: PhosphorIconsLight.circleHalf, read: (state) => state.highContrast, @@ -123,7 +121,7 @@ final _personalizationSettingsPage = SettingsLeapPage( ), SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).nativeTitleBar, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).nativeTitleBarDescription, icon: PhosphorIconsLight.appWindow, enabled: (context, state) => !kIsWeb && isWindow, diff --git a/app/lib/settings/pages/view.dart b/app/lib/settings/pages/view.dart index 0d32d61a5bf3..87a27e6c3af8 100644 --- a/app/lib/settings/pages/view.dart +++ b/app/lib/settings/pages/view.dart @@ -7,32 +7,28 @@ final _viewSettingsPage = SettingsLeapPage( sections: { 'interface': SettingsLeapSection( settings: [ - SettingsLeapBoolSetting( + SettingsLeapAdvancedSwitchSetting( id: 'zoomControl', displayName: (context) => AppLocalizations.of(context).zoomControl, - descriptionBuilder: (context) => - AppLocalizations.of(context).zoomControlDescription, icon: PhosphorIconsLight.magnifyingGlass, - read: (state) => state.zoomEnabled, - write: (context, value) => + options: [ + for (final value in ZoomPosition.values) + SettingsLeapOption( + id: value.name, + value: value, + displayName: (context) => value.getLocalizedName(context), + ), + ], + readEnabled: (state) => state.zoomEnabled, + writeEnabled: (context, value) => context.read().changeZoomEnabled(value), - ), - SettingsLeapEnumSetting( - id: 'zoomPosition', - displayName: (context) => AppLocalizations.of(context).zoomPosition, - descriptionBuilder: (context) => - AppLocalizations.of(context).zoomPositionDescription, - icon: PhosphorIconsLight.arrowsOut, - enabled: (context, state) => state.zoomEnabled, - values: ZoomPosition.values, read: (state) => state.zoomPosition, write: (context, value) => context.read().changeZoomPosition(value), - valueLabel: (context, value) => value.getLocalizedName(context), ), SettingsLeapEnumSetting( displayName: (context) => AppLocalizations.of(context).properties, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).propertiesDescription, icon: PhosphorIconsLight.sliders, values: ZoomPosition.values, @@ -44,8 +40,6 @@ final _viewSettingsPage = SettingsLeapPage( SettingsLeapEnumSetting( displayName: (context) => AppLocalizations.of(context).toolbarPosition, - descriptionBuilder: (context) => - AppLocalizations.of(context).toolbarPositionDescription, icon: PhosphorIconsLight.toolbox, values: ToolbarPosition.values, read: (state) => state.toolbarPosition, @@ -55,8 +49,6 @@ final _viewSettingsPage = SettingsLeapPage( ), SettingsLeapEnumSetting( displayName: (context) => AppLocalizations.of(context).toolbarSize, - descriptionBuilder: (context) => - AppLocalizations.of(context).toolbarSizeDescription, icon: PhosphorIconsLight.toolbox, values: ToolbarSize.values, read: (state) => state.toolbarSize, @@ -66,37 +58,34 @@ final _viewSettingsPage = SettingsLeapPage( ), SettingsLeapCustomSetting( displayName: (context) => AppLocalizations.of(context).toolbarRows, - descriptionBuilder: (context) => - AppLocalizations.of(context).toolbarRowsDescription, builder: _toolbarRowsSetting, ), - SettingsLeapBoolSetting( + SettingsLeapAdvancedSwitchSetting( id: 'navigationRail', displayName: (context) => AppLocalizations.of(context).navigationRail, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).navigationRailDescription, icon: PhosphorIconsLight.sidebar, - read: (state) => state.navigationRail, - write: (context, value) => + options: [ + for (final value in NavigatorPosition.values) + SettingsLeapOption( + id: value.name, + value: value, + displayName: (context) => + _navigatorPositionName(context, value), + ), + ], + readEnabled: (state) => state.navigationRail, + writeEnabled: (context, value) => context.read().changeNavigationRail(value), - ), - SettingsLeapEnumSetting( - id: 'navigatorPosition', - displayName: (context) => AppLocalizations.of(context).position, - descriptionBuilder: (context) => - AppLocalizations.of(context).navigatorPositionDescription, - icon: PhosphorIconsLight.sidebar, - enabled: (context, state) => state.navigationRail, - values: NavigatorPosition.values, read: (state) => state.navigatorPosition, write: (context, value) => context.read().changeNavigatorPosition(value), - valueLabel: _navigatorPositionName, ), SettingsLeapEnumSetting( displayName: (context) => AppLocalizations.of(context).optionsPanelPosition, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).optionsPanelPositionDescription, icon: PhosphorIconsLight.archive, values: OptionsPanelPosition.values, @@ -108,7 +97,7 @@ final _viewSettingsPage = SettingsLeapPage( SettingsLeapEnumSetting( displayName: (context) => AppLocalizations.of(context).simpleToolbarVisibility, - descriptionBuilder: (context) => + hintBuilder: (context) => AppLocalizations.of(context).simpleToolbarVisibilityDescription, icon: PhosphorIconsLight.cursorText, values: SimpleToolbarVisibility.values, @@ -125,8 +114,6 @@ final _viewSettingsPage = SettingsLeapPage( settings: [ SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).showThumbnails, - descriptionBuilder: (context) => - AppLocalizations.of(context).showThumbnailsDescription, icon: PhosphorIconsLight.image, read: (state) => state.showThumbnails, write: (context, value) => @@ -135,8 +122,6 @@ final _viewSettingsPage = SettingsLeapPage( SettingsLeapBoolSetting( displayName: (context) => AppLocalizations.of(context).hideFileExtension, - descriptionBuilder: (context) => - AppLocalizations.of(context).hideFileExtensionDescription, icon: PhosphorIconsLight.fileText, read: (state) => state.hideExtension, write: (context, value) => diff --git a/app/pubspec.lock b/app/pubspec.lock index 155a809f257e..0533d6979483 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -836,8 +836,8 @@ packages: dependency: "direct main" description: path: "packages/material_leap" - ref: bc69ad6ef16c6730ba52dfd466ae7d0c0af2e082 - resolved-ref: bc69ad6ef16c6730ba52dfd466ae7d0c0af2e082 + ref: "606c5977e7d7d0197fdec2d15dbc951afe330b43" + resolved-ref: "606c5977e7d7d0197fdec2d15dbc951afe330b43" url: "https://github.com/LinwoodDev/dart_pkgs.git" source: git version: "0.0.1" @@ -1274,8 +1274,8 @@ packages: dependency: "direct main" description: path: "packages/settings_leap" - ref: "2ecec31542ccafd7a0976f1ba995676b2d8254ba" - resolved-ref: "2ecec31542ccafd7a0976f1ba995676b2d8254ba" + ref: "0840def8828c6597391522c405fa5792a946e53e" + resolved-ref: "0840def8828c6597391522c405fa5792a946e53e" url: "https://github.com/LinwoodDev/dart_pkgs.git" source: git version: "0.1.0" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 348eaeb65d1a..c2c55dde12c7 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -63,12 +63,12 @@ dependencies: settings_leap: git: url: https://github.com/LinwoodDev/dart_pkgs.git - ref: 2ecec31542ccafd7a0976f1ba995676b2d8254ba + ref: 0840def8828c6597391522c405fa5792a946e53e path: packages/settings_leap material_leap: git: url: https://github.com/LinwoodDev/dart_pkgs.git - ref: bc69ad6ef16c6730ba52dfd466ae7d0c0af2e082 + ref: 606c5977e7d7d0197fdec2d15dbc951afe330b43 path: packages/material_leap lw_sysapi: git: From 02b0ecdbbbee7d9004b03b9b894eb56a1666ed03 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sat, 11 Jul 2026 18:18:24 +0200 Subject: [PATCH 052/117] Improve settings navigation and persistent responsiveness --- app/lib/main.dart | 40 +-- .../settings/pages/behaviors/persistence.dart | 326 ++++++++++-------- app/pubspec.lock | 4 +- app/pubspec.yaml | 2 +- 4 files changed, 214 insertions(+), 158 deletions(-) diff --git a/app/lib/main.dart b/app/lib/main.dart index f82f8d21f008..2b6111143b14 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -131,39 +131,39 @@ class ButterflyApp extends StatelessWidget { this.debugShowCheckedModeBanner = true, }); - List _buildSettingsRoute( + List _buildSettingsRoutes( SettingsLeapTree tree, ) { - List buildEntries( + Iterable buildEntries( Map> entries, [ + String? parentPath, String? parentId, - ]) { - return entries.entries.map((entry) { + ]) sync* { + for (final entry in entries.entries) { final id = entry.key; + final path = parentPath == null ? id : '$parentPath/$id'; final fullId = parentId == null ? id : '$parentId.$id'; final page = entry.value; - final children = page.children; - return GoRoute( - path: id, + yield GoRoute( + path: 'settings/$path', builder: (context, state) => SettingsDetailsPage( id: fullId, focusedId: state.extra is String ? state.extra as String : null, ), - routes: [ - ...buildEntries(children, fullId), - if (id == 'connections') - GoRoute( - path: ':id', - name: 'connection', - builder: (context, state) => - ConnectionSettingsPage(remote: state.pathParameters['id']!), - ), - ], ); - }).toList(); + yield* buildEntries(page.children, path, fullId); + if (id == 'connections') { + yield GoRoute( + path: 'settings/$path/:id', + name: 'connection', + builder: (context, state) => + ConnectionSettingsPage(remote: state.pathParameters['id']!), + ); + } + } } - return buildEntries(tree.pages); + return buildEntries(tree.pages).toList(); } late final GoRouter _router = GoRouter( @@ -184,8 +184,8 @@ class ButterflyApp extends StatelessWidget { GoRoute( path: 'settings', builder: (context, state) => const SettingsPage(), - routes: _buildSettingsRoute(settingsTree), ), + ..._buildSettingsRoutes(settingsTree), GoRoute( name: 'new', path: 'new', diff --git a/app/lib/settings/pages/behaviors/persistence.dart b/app/lib/settings/pages/behaviors/persistence.dart index ebd61b000df9..3e60bd6b137f 100644 --- a/app/lib/settings/pages/behaviors/persistence.dart +++ b/app/lib/settings/pages/behaviors/persistence.dart @@ -5,153 +5,209 @@ final _persistenceSettingsPage = SettingsLeapPage( AppLocalizations.of(context).persistenceDocumentStates, icon: PhosphorIconsLight.database, appBarBuilder: _butterflyAppBar, - builder: _buildPersistenceSettingsPage, + sections: { + 'content': SettingsLeapSection( + builder: _buildPersistenceSettingsSection, + wrapBuilder: false, + ), + }, ); -Widget _buildPersistenceSettingsPage( +Widget _buildPersistenceSettingsSection( BuildContext context, ButterflySettings state, - bool inView, + Widget child, ) { final settings = state.documentStatePersistence; void change(DocumentStatePersistenceSettings next) { context.read().changeDocumentStatePersistence(next); } - return ListView( - children: [ - SwitchListTile( - value: settings.enabled, - secondary: const PhosphorIcon(PhosphorIconsLight.power), - title: Text(AppLocalizations.of(context).persistentStatesEnabled), - subtitle: Text( - AppLocalizations.of(context).persistentStatesEnabledDescription, - ), - onChanged: (value) => change(settings.copyWith(enabled: value)), - ), - const Divider(), - SwitchListTile( - value: settings.page, - secondary: const PhosphorIcon(PhosphorIconsLight.file), - title: Text(AppLocalizations.of(context).persistentStateCurrentPage), - subtitle: Text( - AppLocalizations.of(context).persistentStateCurrentPageDescription, - ), - onChanged: settings.enabled - ? (value) => change(settings.copyWith(page: value)) - : null, - ), - SwitchListTile( - value: settings.camera, - secondary: const PhosphorIcon(PhosphorIconsLight.frameCorners), - title: Text(AppLocalizations.of(context).persistentStateViewport), - subtitle: Text( - AppLocalizations.of(context).persistentStateViewportDescription, - ), - onChanged: settings.enabled - ? (value) => change(settings.copyWith(camera: value)) - : null, - ), - SwitchListTile( - value: settings.locks, - secondary: const PhosphorIcon(PhosphorIconsLight.lockKey), - title: Text(AppLocalizations.of(context).lock), - subtitle: Text( - AppLocalizations.of(context).persistentStateLocksDescription, - ), - onChanged: settings.enabled - ? (value) => change(settings.copyWith(locks: value)) - : null, - ), - SwitchListTile( - value: settings.tool, - secondary: const PhosphorIcon(PhosphorIconsLight.toolbox), - title: Text(AppLocalizations.of(context).persistentStateSelectedTool), - subtitle: Text( - AppLocalizations.of(context).persistentStateSelectedToolDescription, - ), - onChanged: settings.enabled - ? (value) => change(settings.copyWith(tool: value)) - : null, - ), - SwitchListTile( - value: settings.navigator, - secondary: const PhosphorIcon(PhosphorIconsLight.sidebar), - title: Text(AppLocalizations.of(context).navigator), - subtitle: Text( - AppLocalizations.of(context).persistentStateNavigatorDescription, - ), - onChanged: settings.enabled - ? (value) => change(settings.copyWith(navigator: value)) - : null, - ), - SwitchListTile( - value: settings.layers, - secondary: const PhosphorIcon(PhosphorIconsLight.stack), - title: Text(AppLocalizations.of(context).layers), - subtitle: Text( - AppLocalizations.of(context).persistentStateLayersDescription, - ), - onChanged: settings.enabled - ? (value) => change(settings.copyWith(layers: value)) - : null, - ), - SwitchListTile( - value: settings.areas, - secondary: const PhosphorIcon(PhosphorIconsLight.selection), - title: Text(AppLocalizations.of(context).areas), - subtitle: Text( - AppLocalizations.of(context).persistentStateAreasDescription, - ), - onChanged: settings.enabled - ? (value) => change(settings.copyWith(areas: value)) - : null, - ), - const Divider(), - ExactSlider( - header: Text(AppLocalizations.of(context).persistentStateMaxRecords), - subtitle: Text( - AppLocalizations.of(context).persistentStateMaxRecordsDescription, - ), - leading: const PhosphorIcon(PhosphorIconsLight.listNumbers), - value: settings.maxEntries.toDouble(), - min: 20, - max: 2000, - defaultValue: 400, - fractionDigits: 0, - onChangeEnd: (value) => - change(settings.copyWith(maxEntries: value.toInt())), - ), - ExactSlider( - header: Text( - AppLocalizations.of(context).persistentStateDeleteOlderThanDays, - ), - subtitle: Text( - AppLocalizations.of( - context, - ).persistentStateDeleteOlderThanDaysDescription, - ), - leading: const PhosphorIcon(PhosphorIconsLight.calendar), - value: settings.maxAgeDays.toDouble(), - min: 7, - max: 730, - defaultValue: 180, - fractionDigits: 0, - onChangeEnd: (value) => - change(settings.copyWith(maxAgeDays: value.toInt())), - ), - ListTile( - leading: const PhosphorIcon(PhosphorIconsLight.trash), - title: Text(AppLocalizations.of(context).persistentStateCleanup), - subtitle: Text( - AppLocalizations.of(context).persistentStateCleanupDescription, + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: LeapBreakpoints.medium), + child: Card( + margin: settingsCardMargin, + child: Padding( + padding: settingsCardPadding, + child: Column( + children: [ + SwitchListTile( + value: settings.enabled, + secondary: const PhosphorIcon(PhosphorIconsLight.power), + title: Text( + AppLocalizations.of(context).persistentStatesEnabled, + ), + subtitle: Text( + AppLocalizations.of( + context, + ).persistentStatesEnabledDescription, + ), + onChanged: (value) => + change(settings.copyWith(enabled: value)), + ), + const Divider(), + SwitchListTile( + value: settings.page, + secondary: const PhosphorIcon(PhosphorIconsLight.file), + title: Text( + AppLocalizations.of(context).persistentStateCurrentPage, + ), + subtitle: Text( + AppLocalizations.of( + context, + ).persistentStateCurrentPageDescription, + ), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(page: value)) + : null, + ), + SwitchListTile( + value: settings.camera, + secondary: const PhosphorIcon( + PhosphorIconsLight.frameCorners, + ), + title: Text( + AppLocalizations.of(context).persistentStateViewport, + ), + subtitle: Text( + AppLocalizations.of( + context, + ).persistentStateViewportDescription, + ), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(camera: value)) + : null, + ), + SwitchListTile( + value: settings.locks, + secondary: const PhosphorIcon(PhosphorIconsLight.lockKey), + title: Text(AppLocalizations.of(context).lock), + subtitle: Text( + AppLocalizations.of( + context, + ).persistentStateLocksDescription, + ), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(locks: value)) + : null, + ), + SwitchListTile( + value: settings.tool, + secondary: const PhosphorIcon(PhosphorIconsLight.toolbox), + title: Text( + AppLocalizations.of(context).persistentStateSelectedTool, + ), + subtitle: Text( + AppLocalizations.of( + context, + ).persistentStateSelectedToolDescription, + ), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(tool: value)) + : null, + ), + SwitchListTile( + value: settings.navigator, + secondary: const PhosphorIcon(PhosphorIconsLight.sidebar), + title: Text(AppLocalizations.of(context).navigator), + subtitle: Text( + AppLocalizations.of( + context, + ).persistentStateNavigatorDescription, + ), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(navigator: value)) + : null, + ), + SwitchListTile( + value: settings.layers, + secondary: const PhosphorIcon(PhosphorIconsLight.stack), + title: Text(AppLocalizations.of(context).layers), + subtitle: Text( + AppLocalizations.of( + context, + ).persistentStateLayersDescription, + ), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(layers: value)) + : null, + ), + SwitchListTile( + value: settings.areas, + secondary: const PhosphorIcon(PhosphorIconsLight.selection), + title: Text(AppLocalizations.of(context).areas), + subtitle: Text( + AppLocalizations.of( + context, + ).persistentStateAreasDescription, + ), + onChanged: settings.enabled + ? (value) => change(settings.copyWith(areas: value)) + : null, + ), + const Divider(), + ExactSlider( + header: Text( + AppLocalizations.of(context).persistentStateMaxRecords, + ), + subtitle: Text( + AppLocalizations.of( + context, + ).persistentStateMaxRecordsDescription, + ), + leading: const PhosphorIcon(PhosphorIconsLight.listNumbers), + value: settings.maxEntries.toDouble(), + min: 20, + max: 2000, + defaultValue: 400, + fractionDigits: 0, + onChangeEnd: (value) => + change(settings.copyWith(maxEntries: value.toInt())), + ), + ExactSlider( + header: Text( + AppLocalizations.of( + context, + ).persistentStateDeleteOlderThanDays, + ), + subtitle: Text( + AppLocalizations.of( + context, + ).persistentStateDeleteOlderThanDaysDescription, + ), + leading: const PhosphorIcon(PhosphorIconsLight.calendar), + value: settings.maxAgeDays.toDouble(), + min: 7, + max: 730, + defaultValue: 180, + fractionDigits: 0, + onChangeEnd: (value) => + change(settings.copyWith(maxAgeDays: value.toInt())), + ), + ListTile( + leading: const PhosphorIcon(PhosphorIconsLight.trash), + title: Text( + AppLocalizations.of(context).persistentStateCleanup, + ), + subtitle: Text( + AppLocalizations.of( + context, + ).persistentStateCleanupDescription, + ), + enabled: settings.enabled, + onTap: settings.enabled + ? () => _cleanupPersistentStates(context) + : null, + ), + ], + ), + ), ), - enabled: settings.enabled, - onTap: settings.enabled - ? () => _cleanupPersistentStates(context) - : null, ), - ], + ), ); } diff --git a/app/pubspec.lock b/app/pubspec.lock index 0533d6979483..dbf7a0d04c40 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -1274,8 +1274,8 @@ packages: dependency: "direct main" description: path: "packages/settings_leap" - ref: "0840def8828c6597391522c405fa5792a946e53e" - resolved-ref: "0840def8828c6597391522c405fa5792a946e53e" + ref: "2aec4a8d2e00f8aab1c55403eab88a5c7db425cf" + resolved-ref: "2aec4a8d2e00f8aab1c55403eab88a5c7db425cf" url: "https://github.com/LinwoodDev/dart_pkgs.git" source: git version: "0.1.0" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index c2c55dde12c7..ea59cea2d5a7 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: settings_leap: git: url: https://github.com/LinwoodDev/dart_pkgs.git - ref: 0840def8828c6597391522c405fa5792a946e53e + ref: 2aec4a8d2e00f8aab1c55403eab88a5c7db425cf path: packages/settings_leap material_leap: git: From 38db4036c45716a5b45b11474c5c08808bbf3e63 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 12 Jul 2026 14:48:32 +0200 Subject: [PATCH 053/117] Fix blur resetting on color change --- api/pubspec.lock | 20 +- app/lib/handlers/laser.dart | 5 +- app/lib/handlers/pen.dart | 5 +- app/lib/handlers/polygon.dart | 4 +- app/lib/handlers/shape.dart | 5 +- app/lib/helpers/color.dart | 2 +- app/lib/views/toolbar/polygon.dart | 5 +- docs/package.json | 6 +- docs/pnpm-lock.yaml | 1607 +++++++++++++++++----------- metadata/en-US/changelogs/188.txt | 1 + 10 files changed, 1010 insertions(+), 650 deletions(-) diff --git a/api/pubspec.lock b/api/pubspec.lock index bfe2e36c3b56..ed4d48100608 100644 --- a/api/pubspec.lock +++ b/api/pubspec.lock @@ -61,34 +61,34 @@ packages: dependency: transitive description: name: build - sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10 + sha256: "45d14a0fb23e018d8287c32fc98d726ce466b231928ed9b9200f29bd3ccd39ae" url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "4.0.7" build_config: dependency: transitive description: name: build_config - sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" + sha256: f2c223156a26eea323e6244b85141d76413a80aeee9fe0b380773789fabaf8ae url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" build_daemon: dependency: transitive description: name: build_daemon - sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + sha256: fd754058c342243718d5171a95f352cfc9fcf0cba8cfa26df67cb13a5836db78 url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6" + sha256: "5367e521935b102bdf1e735d2aab461e36b2edca6517662d088dd04cc39f8d16" url: "https://pub.dev" source: hosted - version: "2.15.0" + version: "2.15.1" built_collection: dependency: transitive description: @@ -319,10 +319,10 @@ packages: dependency: transitive description: name: meta - sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" url: "https://pub.dev" source: hosted - version: "1.18.3" + version: "1.19.0" mime: dependency: transitive description: diff --git a/app/lib/handlers/laser.dart b/app/lib/handlers/laser.dart index 74be883b0c27..61cfb15b2f60 100644 --- a/app/lib/handlers/laser.dart +++ b/app/lib/handlers/laser.dart @@ -49,7 +49,10 @@ class LaserHandler extends Handler with ColoredHandler { color = color.withValues(a: alpha); return element.copyWith( property: element.property.copyWith( - paint: ElementPaint.solid(color: color), + paint: ElementPaint.solid( + color: color, + blur: element.property.paint.blur, + ), ), ); } diff --git a/app/lib/handlers/pen.dart b/app/lib/handlers/pen.dart index fcf18db2e3bc..6695af223bba 100644 --- a/app/lib/handlers/pen.dart +++ b/app/lib/handlers/pen.dart @@ -475,7 +475,10 @@ class PenHandler extends Handler with ColoredHandler { @override PenTool setColor(SRGBColor color) => data.copyWith( property: data.property.copyWith( - paint: ElementPaint.solid(color: color.withValues(a: getColor().a)), + paint: ElementPaint.solid( + color: color.withValues(a: getColor().a), + blur: data.property.paint.blur, + ), ), ); diff --git a/app/lib/handlers/polygon.dart b/app/lib/handlers/polygon.dart index c2e329b4c638..764b55ccc63a 100644 --- a/app/lib/handlers/polygon.dart +++ b/app/lib/handlers/polygon.dart @@ -105,7 +105,9 @@ class PolygonHandler extends Handler with ColoredHandler { @override PolygonTool setColor(SRGBColor color) => data.copyWith( - property: data.property.copyWith(paint: ElementPaint.solid(color: color)), + property: data.property.copyWith( + paint: ElementPaint.solid(color: color, blur: data.property.paint.blur), + ), ); @override diff --git a/app/lib/handlers/shape.dart b/app/lib/handlers/shape.dart index e030b8192da3..89355d5b8ef9 100644 --- a/app/lib/handlers/shape.dart +++ b/app/lib/handlers/shape.dart @@ -68,7 +68,10 @@ class ShapeHandler extends PastingHandler with ColoredHandler { @override ShapeTool setColor(SRGBColor color) => data.copyWith( property: data.property.copyWith( - paint: ElementPaint.solid(color: color.withValues(a: getColor().a)), + paint: ElementPaint.solid( + color: color.withValues(a: getColor().a), + blur: data.property.paint.blur, + ), ), ); diff --git a/app/lib/helpers/color.dart b/app/lib/helpers/color.dart index 572ccaa6268a..8b4ba34ff3cb 100644 --- a/app/lib/helpers/color.dart +++ b/app/lib/helpers/color.dart @@ -22,7 +22,7 @@ ElementPaint _updatePaintDefaultColor( SolidElementPaint e => e.copyWith( color: _updateColor(e.color, defaultColor, force: force), ), - _ when force => ElementPaint.solid(color: defaultColor), + _ when force => ElementPaint.solid(color: defaultColor, blur: paint.blur), _ => paint, }; } diff --git a/app/lib/views/toolbar/polygon.dart b/app/lib/views/toolbar/polygon.dart index c5cacf2784c4..880d0c40678e 100644 --- a/app/lib/views/toolbar/polygon.dart +++ b/app/lib/views/toolbar/polygon.dart @@ -32,7 +32,10 @@ class PolygonToolbarView extends StatelessWidget onChanged: (value) => onToolChanged.call( tool.copyWith( property: tool.property.copyWith( - paint: ElementPaint.solid(color: value), + paint: ElementPaint.solid( + color: value, + blur: tool.property.paint.blur, + ), ), ), ), diff --git a/docs/package.json b/docs/package.json index bdd3e655b5b7..8764e8249f52 100644 --- a/docs/package.json +++ b/docs/package.json @@ -18,13 +18,13 @@ "@phosphor-icons/react": "^2.1.10", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", - "astro": "^7.0.6", + "astro": "^7.0.7", "katex": "^0.17.0", "react": "^19.2.7", "react-dom": "^19.2.7", - "typescript": "^6.0.3" + "typescript": "^7.0.2" }, - "packageManager": "pnpm@11.10.0", + "packageManager": "pnpm@11.12.0", "devDependencies": { "@vite-pwa/astro": "^1.2.0", "sass": "^1.101.0", diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index eae677d30882..d22de8b8c7da 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@astrojs/check': specifier: ^0.9.9 - version: 0.9.9(prettier@3.9.4)(typescript@6.0.3) + version: 0.9.9(prettier@3.9.5)(typescript@7.0.2) '@astrojs/markdown-satteri': specifier: ^0.3.3 version: 0.3.3 @@ -19,7 +19,7 @@ importers: version: 6.0.1(@types/node@26.1.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) '@astrojs/starlight': specifier: ^0.41.3 - version: 0.41.3(@astrojs/markdown-remark@7.2.1)(astro@7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3) + version: 0.41.3(@astrojs/markdown-remark@7.2.1)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@7.0.2) '@linwooddev/style': specifier: github:LinwoodDev/style#efbdf8f05d4ef1cf6fec65c90194d30b68b3c64e&path:/packages/web version: https://codeload.github.com/LinwoodDev/style/tar.gz/efbdf8f05d4ef1cf6fec65c90194d30b68b3c64e#path:/packages/web @@ -33,8 +33,8 @@ importers: specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.17) astro: - specifier: ^7.0.6 - version: 7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + specifier: ^7.0.7 + version: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) katex: specifier: ^0.17.0 version: 0.17.0 @@ -45,12 +45,12 @@ importers: specifier: ^19.2.7 version: 19.2.7(react@19.2.7) typescript: - specifier: ^6.0.3 - version: 6.0.3 + specifier: ^7.0.2 + version: 7.0.2 devDependencies: '@vite-pwa/astro': specifier: ^1.2.0 - version: 1.2.0(astro@7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1)) + version: 1.2.0(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1)) sass: specifier: ^1.101.0 version: 1.101.0 @@ -59,7 +59,7 @@ importers: version: 0.35.3(@types/node@26.1.0) vite-plugin-pwa: specifier: ^1.3.0 - version: 1.3.0(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1) + version: 1.3.0(vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1) workbox-window: specifier: ^7.4.1 version: 7.4.1 @@ -78,69 +78,69 @@ packages: peerDependencies: typescript: ^5.0.0 || ^6.0.0 - '@astrojs/compiler-binding-darwin-arm64@0.3.0': - resolution: {integrity: sha512-3n0uu+uJpnCq8b4JFi3uGDsIisAvHctxSmH+cIO9Gbei1H1Y1QXaYboXyiWJugUmprr3OEYP7+LdodzpVFzLMQ==} + '@astrojs/compiler-binding-darwin-arm64@0.3.1': + resolution: {integrity: sha512-IEmEF2fUIlTHtpeE/isyEGVOB14cEyh/LZOFYt6wn3jNyVpdC8aR5OZ+RzFUR/f+8ZDM1LaMwZKvoA7eMyJeFw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@astrojs/compiler-binding-darwin-x64@0.3.0': - resolution: {integrity: sha512-scxNGKjOBydMo1QR4LtK0FMgh7ubQomJDv953nz2msQFkPKke/0FpPv/cQM0T/kuZdReZQFU8Oz3iOrP/6WHEg==} + '@astrojs/compiler-binding-darwin-x64@0.3.1': + resolution: {integrity: sha512-GF2kIxjpPDLsn94zbZNMsxEmkU828QqnmM7kiQJnaooS3jmI+I7kk6+oI6EpwOsK3femCMdcm+wmOsEqtGrmjQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@astrojs/compiler-binding-linux-arm64-gnu@0.3.0': - resolution: {integrity: sha512-NZrWLolVUANmrnl0zrFK/Sx5Sock1gEUT49ALfMTTCA5Ya2ec/BoJXMIg4KgE+wZcrdXJ8e+WyEhM7YLk/FJkA==} + '@astrojs/compiler-binding-linux-arm64-gnu@0.3.1': + resolution: {integrity: sha512-XJL3SDmOtVrqFhCirNcHwE91+IesJqlgNo23I4qW9QUYfwzm/TBZuH61fgqsb1ttgR1mMYz6ooPWs0JDhwMqpQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@astrojs/compiler-binding-linux-arm64-musl@0.3.0': - resolution: {integrity: sha512-PjwRmKgMFDsFhg82g0poXlIY8Qn3fMA3hXjaR0coJWJzTJsRH9ATU0j2ocigjtU1h3vL/yR7yLUxGj/lTCq73g==} + '@astrojs/compiler-binding-linux-arm64-musl@0.3.1': + resolution: {integrity: sha512-xqE8BVbDoBueK/B47w30PtkVofUWJKGkwoMVE+EOMLf11rnoANxIAdA9FPqY+rng4oNI5ndHGsri1yPj2k8vZQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@astrojs/compiler-binding-linux-x64-gnu@0.3.0': - resolution: {integrity: sha512-Dr69VJYlnSfyL8gzELW6S4mE41P7TDPn1IKjwMnjdZ7+dxgJI50oMLFSk1LVe26bHmWB3ktuh8fDVK1THI9e9A==} + '@astrojs/compiler-binding-linux-x64-gnu@0.3.1': + resolution: {integrity: sha512-1y0StU1qiCuDFH3rmbRJXcxdfHxFPrES1Rd+RLffosvUR7I2cH5SF5SFnBN9vXpzpkmyElZm3Yr47iJBPN7vVA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@astrojs/compiler-binding-linux-x64-musl@0.3.0': - resolution: {integrity: sha512-AEt+bRw8PfImCcyRH1lpXVB8CdmQ1K/wPo5u99iec4/U/XdNvQZ715YVuNzIJpbJXelgQeZ5H2+Ea7XwRyWY5g==} + '@astrojs/compiler-binding-linux-x64-musl@0.3.1': + resolution: {integrity: sha512-16q0fYf7kpbmdObZEeZJEup8hQv/whgNwVjrSvT8umrKwLDSnNIWiQpm09lQQu6bweZB0XyIvHwlPitvJhC+hg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@astrojs/compiler-binding-wasm32-wasi@0.3.0': - resolution: {integrity: sha512-U80tA1j8V6LjhiTZzVCtG4E8hrNVVNXDGV5fCgJ94q8FU9CPH+XwdDDhLzBybfWhKfyItXmQiZNRPTiPCYTpVg==} + '@astrojs/compiler-binding-wasm32-wasi@0.3.1': + resolution: {integrity: sha512-cB456shIwDv/PrVT+2QG7LFndpHkVge5HjqADKZgGaAc9JHVktCtjSrcdkRQ+3tbkPazNKaTLRjXLIiz2NIx9g==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@astrojs/compiler-binding-win32-arm64-msvc@0.3.0': - resolution: {integrity: sha512-CpY1RII2r1XMpOUVD1VR/F2wtuRsiOCkFULS10Khyj8/DFZMtxVuUCAWGw+CW2Ka0h6eP3Xc1CA+glFlvXMPxA==} + '@astrojs/compiler-binding-win32-arm64-msvc@0.3.1': + resolution: {integrity: sha512-ur/9+If/yTE69mmeX5MqSZndL0HOyx67GeNZUy3N7wVdWpLz9UTJXwyWS4UR2PUQHitghjsM5xoX0Ge56WRVQQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@astrojs/compiler-binding-win32-x64-msvc@0.3.0': - resolution: {integrity: sha512-qmFbs769oeeGrRebAnCW7aBk8m71vf85W/dX/jddfx5Z06/w0wf7TZCfJPOX1Fld2t+4N+iXzfGEJG+zJQ+bzg==} + '@astrojs/compiler-binding-win32-x64-msvc@0.3.1': + resolution: {integrity: sha512-k0W+kDBzDkNZOqu4kElDvCOIbKw5Ut9S1WZ1Krj3KTgNuBERNKXsMMsRLLcbgfdMdbe7bTekQLshZrrvmYpmwA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@astrojs/compiler-binding@0.3.0': - resolution: {integrity: sha512-zlsOT5COD9hRwplJCgQhS21unxON5AKirf0vgt1ijXwuseYIaZdm2ZOpF8fsz+DY9EyXx+I/ukxtg7uoBep68A==} + '@astrojs/compiler-binding@0.3.1': + resolution: {integrity: sha512-DaAUj29AIBU2XdJ8uwcab8lW5O2pk9pY8AXkcMw0sw77nVa3oeTYRcO+Dvbbpoexf6ThMc0FMWYCQ/wN1/T7oQ==} engines: {node: ^20.19.0 || >=22.12.0} - '@astrojs/compiler-rs@0.3.0': - resolution: {integrity: sha512-J2qEVHtIDjEM9TxwmwuebOGmZNwhKu/dR7P7qBpnJKGmBBX0vdweQ/4cEXhj8fBbWVUB5V12xWChri3CgKNULQ==} + '@astrojs/compiler-rs@0.3.1': + resolution: {integrity: sha512-aT7xkgsbNoS6nriY5qKpbihK43slFHO41iqgHCTdOvn1ifaQxLCc5yXy+6GzAtiafoaC1zA7OwVXCXMsvUZOkg==} engines: {node: '>=22.12.0'} '@astrojs/compiler@2.13.1': @@ -202,8 +202,8 @@ packages: '@astrojs/markdown-remark': optional: true - '@astrojs/telemetry@3.3.2': - resolution: {integrity: sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ==} + '@astrojs/telemetry@3.3.3': + resolution: {integrity: sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} '@astrojs/yaml2ts@0.2.4': @@ -722,52 +722,52 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} - '@bruits/satteri-darwin-arm64@0.9.4': - resolution: {integrity: sha512-W3MSUkr2mZRR8Stoe+lqNAyzQzRuFMU8WffV9IvFSxTok0LGWR0ZZQPLELU4QTRiUbhL2Y4VUP9vV7pj8rHjgg==} + '@bruits/satteri-darwin-arm64@0.9.5': + resolution: {integrity: sha512-iw4nZgx9v30lWo/MTngQqi1pI78KI0DnkSm+lVJGYdmPLgAyDNJigVhpG42/Iq55A6c1Ll8q66ljyyRiQUxwow==} cpu: [arm64] os: [darwin] - '@bruits/satteri-darwin-x64@0.9.4': - resolution: {integrity: sha512-DXOuuaE1lsv7mpk2mOvGrzqoEWEvOIZEO/fXVa7zfM23Iob+CBjBkRAMwpHA4pmZ3j6Gj7WJzPKw0kQ7w741AQ==} + '@bruits/satteri-darwin-x64@0.9.5': + resolution: {integrity: sha512-6T26Z5Kf3cFW2PSlk9p7zT7yVxvuBSiJvYyz9u8KjYwMTqZyIDOj2wDyNpxKV4+6yUVG7rddq2QwvG/8LJA2+Q==} cpu: [x64] os: [darwin] - '@bruits/satteri-linux-arm64-gnu@0.9.4': - resolution: {integrity: sha512-gJxU9rGGoqIznSEgEzpjxkry24jeHuMpoo1tCIAhHYh7WaD3j5F8zt3jmHxEaN1Uwa+K5+wFgIR2uIGOnMzEmw==} + '@bruits/satteri-linux-arm64-gnu@0.9.5': + resolution: {integrity: sha512-u51id17uJwNEMK9nBlICsq6U31c+XVqQueVBkwRIzZG+gMpS8TOJctt5h5Wz33Z8xnMdTd+adtACVz0yHgGuOA==} cpu: [arm64] os: [linux] libc: [glibc] - '@bruits/satteri-linux-arm64-musl@0.9.4': - resolution: {integrity: sha512-Wjzu9hmmAbfmDkBfPI1VdZygJtYz9uYZQnkEyrXi6S2JFi+2pXQ1A5irj38bqm0IZmWcTbk0cVG4NZnPdtVNJA==} + '@bruits/satteri-linux-arm64-musl@0.9.5': + resolution: {integrity: sha512-v39HxiwGC5Rqm01HksP6+5Y+xKLPlsuVFgIgpEAo+SiQ22c+mJVhS3u7Z6ePAKdhL5NJoK1xq70kLz3L13AhpQ==} cpu: [arm64] os: [linux] libc: [musl] - '@bruits/satteri-linux-x64-gnu@0.9.4': - resolution: {integrity: sha512-MR1Q+wMx65FQlbSV7cRqWW87Knp0zkoaIV55Dt+xZl028wJABXEPEEmG3670SLq7lVZvcGIDwCgSg2kCYxvRwA==} + '@bruits/satteri-linux-x64-gnu@0.9.5': + resolution: {integrity: sha512-F3uO8uFp3pAP5ZGXttwvh57GS7s0lL953tnNdyI2gRyP4kOOkp6pyGojNJzCjkDvWI2Cvb9iNrKok3aqQPauAw==} cpu: [x64] os: [linux] libc: [glibc] - '@bruits/satteri-linux-x64-musl@0.9.4': - resolution: {integrity: sha512-T4gxhXve3zyNAZesrXAd/rDZOGRkbfFIUFld4TGsw6BsjoIteCcDji6IMqeXyaWEVSykY2X8Eid2hr6aXGYAaw==} + '@bruits/satteri-linux-x64-musl@0.9.5': + resolution: {integrity: sha512-bicEqglLlz++mWyADaZoP0JY20s4vDfLjaPYgQqC+NI4zZLTOOg1T4GB8aqtc822Pqji8SQBmSrTb7CrP8i08Q==} cpu: [x64] os: [linux] libc: [musl] - '@bruits/satteri-wasm32-wasi@0.9.4': - resolution: {integrity: sha512-/CEG8LUlpaBEnhFnYVn0UnlHFLs51UhrkJBUPDUXLzkadzAcnR88iRA/nOl7Zwhjb4WhfBV4p3P5qeOJMtH0iA==} + '@bruits/satteri-wasm32-wasi@0.9.5': + resolution: {integrity: sha512-zauAuMwfPnKPUkd4AFixRFpXdgKwP2mKgxrIIo2gJzW0/ZneF9dbHnLkojSpaBnCCp7VUL1hIi5WWZvB1CqmAQ==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@bruits/satteri-win32-arm64-msvc@0.9.4': - resolution: {integrity: sha512-E1ZPQbgCtFKiU7pFYVndynvY7ne4coeVDUgnVThErSFlJ2ceQCBZrfRTD1lzrIDy63Bbqo+g/cZY9duw+JYjIw==} + '@bruits/satteri-win32-arm64-msvc@0.9.5': + resolution: {integrity: sha512-SrfE7NEsgZjBvU3c+RR6oQRu0ToXY5uVJEbieXEF0YTctIV2zAVlbaMjWLts074QCgh3a+XHWkR/lWh2VH2LUg==} cpu: [arm64] os: [win32] - '@bruits/satteri-win32-x64-msvc@0.9.4': - resolution: {integrity: sha512-5I7SiarsNdAUuhJb50CXJPTwr/ECVrBoU+fymoLjChK5fW//+srhY4lstcNTzgFRtQSYfVtm4OQZz16CVMeTeA==} + '@bruits/satteri-win32-x64-msvc@0.9.5': + resolution: {integrity: sha512-5Kw9ZAtTGS8WHizyn+CJhjjfIQrw+7jcZodpmpXJjefnO15M8UexIi6JR2E5thyvsmHyhL6ZDDMUNR4bKJPd4g==} cpu: [x64] os: [win32] @@ -1150,6 +1150,10 @@ packages: cpu: [x64] os: [win32] + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -1185,8 +1189,8 @@ packages: '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} - '@oxc-project/types@0.137.0': - resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} '@pagefind/darwin-arm64@1.5.2': resolution: {integrity: sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==} @@ -1321,97 +1325,97 @@ packages: react: '>= 16.8' react-dom: '>= 16.8' - '@rolldown/binding-android-arm64@1.1.3': - resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==} + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.1.3': - resolution: {integrity: sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==} + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.1.3': - resolution: {integrity: sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==} + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.1.3': - resolution: {integrity: sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==} + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.1.3': - resolution: {integrity: sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==} + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.1.3': - resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==} + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.1.3': - resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==} + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.1.3': - resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==} + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.1.3': - resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==} + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.1.3': - resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==} + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.1.3': - resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==} + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.1.3': - resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==} + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.1.3': - resolution: {integrity: sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==} + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.1.3': - resolution: {integrity: sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==} + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.3': - resolution: {integrity: sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==} + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1422,19 +1426,21 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - '@rollup/plugin-babel@5.3.1': - resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} - engines: {node: '>= 10.0.0'} + '@rollup/plugin-babel@6.1.0': + resolution: {integrity: sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==} + engines: {node: '>=14.0.0'} peerDependencies: '@babel/core': ^7.0.0 '@types/babel__core': ^7.1.9 - rollup: ^1.20.0||^2.0.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: '@types/babel__core': optional: true + rollup: + optional: true - '@rollup/plugin-node-resolve@15.3.1': - resolution: {integrity: sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==} + '@rollup/plugin-node-resolve@16.0.3': + resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} engines: {node: '>=14.0.0'} peerDependencies: rollup: ^2.78.0||^3.0.0||^4.0.0 @@ -1442,25 +1448,23 @@ packages: rollup: optional: true - '@rollup/plugin-replace@2.4.2': - resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==} - peerDependencies: - rollup: ^1.20.0 || ^2.0.0 - - '@rollup/plugin-terser@0.4.4': - resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} + '@rollup/plugin-replace@6.0.3': + resolution: {integrity: sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^2.0.0||^3.0.0||^4.0.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true - '@rollup/pluginutils@3.1.0': - resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} - engines: {node: '>= 8.0.0'} + '@rollup/plugin-terser@1.0.0': + resolution: {integrity: sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==} + engines: {node: '>=20.0.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0 + rollup: ^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true '@rollup/pluginutils@5.4.0': resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} @@ -1471,58 +1475,168 @@ packages: rollup: optional: true - '@shikijs/core@4.3.0': - resolution: {integrity: sha512-EooU3i9F6IAE8kEu+AnGf9DFZWkQBZ+hJn3tLVbsH+61mtQiva5biai66fAA6nvFPXkLgvrh7BrR7YcJU83xQQ==} - engines: {node: '>=20'} + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] '@shikijs/core@4.3.1': resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==} engines: {node: '>=20'} - '@shikijs/engine-javascript@4.3.0': - resolution: {integrity: sha512-hTv/KiFf2tpiqlACPiztGGurEARWIutB8YUhcrA1pUC7VzzwKO+g5crUocrLztrZ5ro5Z4hbXg7bYclETn3gSQ==} - engines: {node: '>=20'} - '@shikijs/engine-javascript@4.3.1': resolution: {integrity: sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==} engines: {node: '>=20'} - '@shikijs/engine-oniguruma@4.3.0': - resolution: {integrity: sha512-1vMdN3gHfnKfLYwecUI2ITJI4RhHt96xEaJumVn7Heb0IlJ8WQMIH0Voak+2j22BpSNKdnOfB/pCTPnPm2gq7A==} - engines: {node: '>=20'} - '@shikijs/engine-oniguruma@4.3.1': resolution: {integrity: sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==} engines: {node: '>=20'} - '@shikijs/langs@4.3.0': - resolution: {integrity: sha512-rnlqFbBRSys9bT4gl/5rw9RnS0W/I84ZldXPkO7cvlEMoV85TyF/aU01N7/NbSR776RNLjrJKjfFUXJR6wN1Cg==} - engines: {node: '>=20'} - '@shikijs/langs@4.3.1': resolution: {integrity: sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==} engines: {node: '>=20'} - '@shikijs/primitive@4.3.0': - resolution: {integrity: sha512-CPkz64PTa5diRW1ggzMZH9VM/du4RNChYgVtgqrFcgruvIybmCvySv8GkiHSczUHXYuuR8TdKEwFx+UnZMpgdg==} - engines: {node: '>=20'} - '@shikijs/primitive@4.3.1': resolution: {integrity: sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==} engines: {node: '>=20'} - '@shikijs/themes@4.3.0': - resolution: {integrity: sha512-Avgt05YiT+Y3prjIc9lmQxhJzHBcCfR6cjiFW4OyaMBbt2A6trX5rfjUzx+Vj/mE9qpArYjatnqo9XPjQNW/AQ==} - engines: {node: '>=20'} - '@shikijs/themes@4.3.1': resolution: {integrity: sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==} engines: {node: '>=20'} - '@shikijs/types@4.3.0': - resolution: {integrity: sha512-oc8b9U2SYvofKZk8e/737nIX0qwf6eV2vHFATeObAu7r+mUVpLs8Re0BmVkIjAWAYgkmG/CzLNo7rzuBzRu/wQ==} - engines: {node: '>=20'} - '@shikijs/types@4.3.1': resolution: {integrity: sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==} engines: {node: '>=20'} @@ -1530,8 +1644,9 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} - '@surma/rollup-plugin-off-main-thread@2.2.3': - resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} + '@trickfilm400/rollup-plugin-off-main-thread@3.0.0-pre1': + resolution: {integrity: sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q==} + engines: {node: '>=12'} '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -1554,15 +1669,15 @@ packages: '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} - '@types/estree@0.0.39': - resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} - '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} @@ -1578,8 +1693,8 @@ packages: '@types/nlcst@2.0.3': resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} - '@types/node@24.13.2': - resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==} + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} '@types/node@26.1.0': resolution: {integrity: sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==} @@ -1607,6 +1722,126 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + '@ungap/structured-clone@1.3.2': resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==} @@ -1724,8 +1959,8 @@ packages: peerDependencies: astro: ^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0 - astro@7.0.6: - resolution: {integrity: sha512-Myw0sFia+zs/Y0yqfZEsUYXfDPh3ELcLf1f0Q/qQzVXBh/af1qO62WNT+P89DCcfGVV51nMoQhEfkBYqJmoUOQ==} + astro@7.0.7: + resolution: {integrity: sha512-swqrKDSI/B83GFroYPZYMFcxqbSe9+tkynu1WDBk3GLgBfV9++qVM4Z+2uFT6uu9A53Q0TROnxLketfMEENuqQ==} engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true peerDependencies: @@ -1774,6 +2009,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + baseline-browser-mapping@2.10.40: resolution: {integrity: sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==} engines: {node: '>=6.0.0'} @@ -1788,12 +2027,13 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@1.1.15: - resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} - brace-expansion@2.1.1: resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} + engines: {node: 18 || 20 || >=22} + browserslist@4.28.4: resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -1884,9 +2124,6 @@ packages: resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} engines: {node: '>=4.0.0'} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -1900,6 +2137,10 @@ packages: core-js-compat@3.49.0: resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + crossws@0.3.5: resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} @@ -2112,9 +2353,6 @@ packages: estree-util-visit@2.0.0: resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} - estree-walker@1.0.1: - resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} - estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -2125,6 +2363,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eta@4.6.0: + resolution: {integrity: sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==} + engines: {node: '>=20'} + eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} @@ -2179,13 +2421,14 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + fs-extra@9.1.0: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2235,9 +2478,11 @@ packages: github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} + engines: {node: 20 || >=22} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} @@ -2348,10 +2593,10 @@ packages: http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} - i18next@26.3.4: - resolution: {integrity: sha512-pa7m0d7pBDqGHZxljT+WPFeyFgQ7P7SciPPo1tTqYuO0z4sqADYhwnBESmmGp/wEof1inwdls/k8ZgTg8rxFHA==} + i18next@26.3.6: + resolution: {integrity: sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==} peerDependencies: - typescript: ^5 || ^6 + typescript: ^5 || ^6 || ^7 peerDependenciesMeta: typescript: optional: true @@ -2362,13 +2607,6 @@ packages: immutable@5.1.9: resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==} - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -2420,11 +2658,6 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} - is-docker@3.0.0: - resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - hasBin: true - is-docker@4.0.0: resolution: {integrity: sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==} engines: {node: '>=20'} @@ -2457,11 +2690,6 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} - is-inside-container@1.0.0: - resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} - engines: {node: '>=14.16'} - hasBin: true - is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -2529,13 +2757,16 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} - is-wsl@3.1.1: - resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} - engines: {node: '>=16'} - isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + jake@10.9.4: resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} engines: {node: '>=10'} @@ -2670,9 +2901,6 @@ packages: lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - lodash@4.18.1: - resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} - longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -2680,12 +2908,13 @@ packages: resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} engines: {node: 20 || >=22} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - magic-string@0.25.9: - resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -2871,13 +3100,18 @@ packages: micromark@4.0.2: resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} - minimatch@3.1.5: - resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} minimatch@5.1.9: resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} engines: {node: '>=10'} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -2942,9 +3176,6 @@ packages: ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - oniguruma-parser@0.12.2: resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} @@ -2967,6 +3198,9 @@ packages: resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} engines: {node: '>=20'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@1.7.0: resolution: {integrity: sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ==} @@ -2986,13 +3220,17 @@ packages: path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + piccolore@0.1.3: resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==} @@ -3003,10 +3241,6 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - picomatch@4.0.5: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} @@ -3029,8 +3263,8 @@ packages: resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} - prettier@3.9.4: - resolution: {integrity: sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==} + prettier@3.9.5: + resolution: {integrity: sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==} engines: {node: '>=14'} hasBin: true @@ -3060,9 +3294,6 @@ packages: radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - react-dom@19.2.7: resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} peerDependencies: @@ -3210,23 +3441,20 @@ packages: retext@9.0.0: resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} - rolldown@1.1.3: - resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==} + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rollup@2.80.0: - resolution: {integrity: sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==} - engines: {node: '>=10.0.0'} + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true safe-array-concat@1.1.4: resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} engines: {node: '>=0.4'} - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -3240,8 +3468,8 @@ packages: engines: {node: '>=20.19.0'} hasBin: true - satteri@0.9.4: - resolution: {integrity: sha512-BKob126Tay84diOZsnVNH/Q/c+3njPJTCad3w5zLKa6j8bVjxskPNHDtxrMwYK4bN/RlqUSdMnPwKY4k65EMOQ==} + satteri@0.9.5: + resolution: {integrity: sha512-ZuWVl+vnM64y+/TtX8Kosv2c00W+hLQiiwnEL6H0UKVVrxFqMw4D2CJHHQaouVd89OAhtBBfjWLqhKi3TVUV4w==} sax@1.6.0: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} @@ -3259,8 +3487,9 @@ packages: engines: {node: '>=10'} hasBin: true - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + serialize-javascript@7.0.7: + resolution: {integrity: sha512-YAy8Od6KV+uuwUuU50np8fGB/Aues6Y0nAhA9y/hId74PlKUcme4pXcBD46NWKr1Q4osN/iseZ17YqO1XfmI8g==} + engines: {node: '>=20.0.0'} set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} @@ -3283,9 +3512,13 @@ packages: '@types/node': optional: true - shiki@4.3.0: - resolution: {integrity: sha512-NKKjWzR6LIGL3sXBrWDw9sDS9cxx42/DkysaNqJEeOWE8Kix5gpak0bc00OfDVEO4oyXSyz8+aRaqKoBD1yo7A==} - engines: {node: '>=20'} + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} shiki@4.3.1: resolution: {integrity: sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==} @@ -3307,6 +3540,10 @@ packages: resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -3343,10 +3580,6 @@ packages: engines: {node: '>= 8'} deprecated: The work that was done in this beta branch won't be included in future versions - sourcemap-codec@1.4.8: - resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} - deprecated: Please use @jridgewell/sourcemap-codec instead - space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -3473,16 +3706,16 @@ packages: typescript-auto-import-cache@0.3.6: resolution: {integrity: sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==} - typescript@6.0.3: - resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} - engines: {node: '>=14.17'} + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} hasBin: true ufo@1.6.4: resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} - ultrahtml@1.6.0: - resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} + ultrahtml@1.7.0: + resolution: {integrity: sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==} unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} @@ -3657,8 +3890,8 @@ packages: '@vite-pwa/assets-generator': optional: true - vite@8.1.3: - resolution: {integrity: sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==} + vite@8.1.4: + resolution: {integrity: sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3831,65 +4064,60 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - which-pm-runs@1.1.0: - resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} - engines: {node: '>=4'} - which-typed-array@1.1.22: resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} engines: {node: '>= 0.4'} - workbox-background-sync@7.3.0: - resolution: {integrity: sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg==} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true - workbox-broadcast-update@7.3.0: - resolution: {integrity: sha512-T9/F5VEdJVhwmrIAE+E/kq5at2OY6+OXXgOWQevnubal6sO92Gjo24v6dCVwQiclAF5NS3hlmsifRrpQzZCdUA==} + workbox-background-sync@7.4.1: + resolution: {integrity: sha512-HhT7KE8tOWDm02wRNshXUnUPofMlhenF2DBdUnDPOubhizzPeItkYTmAB6td1Z2cjYPa98vzEiPLEuzn5hN66g==} - workbox-build@7.3.0: - resolution: {integrity: sha512-JGL6vZTPlxnlqZRhR/K/msqg3wKP+m0wfEUVosK7gsYzSgeIxvZLi1ViJJzVL7CEeI8r7rGFV973RiEqkP3lWQ==} - engines: {node: '>=16.0.0'} + workbox-broadcast-update@7.4.1: + resolution: {integrity: sha512-uAlgslKLvbQY+suirIdnBCSYrcgBhjp81Nj4l1lj/Jmj0MJO2CJERnCJjT0GFVwmReV0N+zs78K6gqd5gr9/+A==} - workbox-cacheable-response@7.3.0: - resolution: {integrity: sha512-eAFERIg6J2LuyELhLlmeRcJFa5e16Mj8kL2yCDbhWE+HUun9skRQrGIFVUagqWj4DMaaPSMWfAolM7XZZxNmxA==} + workbox-build@7.4.1: + resolution: {integrity: sha512-SDhxIvEAde9Gy/5w4Yo1Jh/M49Z0qE3q0oteyE8zGq0DScxFqVBcCtIXFuLtmtxRQZCMbf0prco4VyEu3KBQuw==} + engines: {node: '>=20.0.0'} - workbox-core@7.3.0: - resolution: {integrity: sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw==} + workbox-cacheable-response@7.4.1: + resolution: {integrity: sha512-8xaFoJdDc2OjrlbbL3gEeBO1WKcMwRqwLRupgqahYXu75yXajPLuwrbXMrIGZuWYXrQwk0xDjOxZ/ujCy/oJYw==} workbox-core@7.4.1: resolution: {integrity: sha512-DT+vu46eh/2vRsSHTY4Xmc32Z1rr9PRlQUXr1Dx30ZuXRWwOsvZgGgcwxcasubQLQmbTNYZjv44LkBAQ4tT5tQ==} - workbox-expiration@7.3.0: - resolution: {integrity: sha512-lpnSSLp2BM+K6bgFCWc5bS1LR5pAwDWbcKt1iL87/eTSJRdLdAwGQznZE+1czLgn/X05YChsrEegTNxjM067vQ==} - - workbox-google-analytics@7.3.0: - resolution: {integrity: sha512-ii/tSfFdhjLHZ2BrYgFNTrb/yk04pw2hasgbM70jpZfLk0vdJAXgaiMAWsoE+wfJDNWoZmBYY0hMVI0v5wWDbg==} + workbox-expiration@7.4.1: + resolution: {integrity: sha512-lRKUF7b+OGbeXkQk1s6MHXOa3d7Xxf7Of31W6c6hCfipfIyrtdWZ89stq21AHZMaoG7VNFoHply4Ox+rU31TWg==} - workbox-navigation-preload@7.3.0: - resolution: {integrity: sha512-fTJzogmFaTv4bShZ6aA7Bfj4Cewaq5rp30qcxl2iYM45YD79rKIhvzNHiFj1P+u5ZZldroqhASXwwoyusnr2cg==} + workbox-google-analytics@7.4.1: + resolution: {integrity: sha512-Mks1JwLEt++ZAkF6sS1OpSh9RtAMIsiDgRpK+codiHGIPXeaUOgi4cPc3GFadUl8V5QPeypEk8Oxgl3HlwVzHw==} - workbox-precaching@7.3.0: - resolution: {integrity: sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw==} + workbox-navigation-preload@7.4.1: + resolution: {integrity: sha512-C4KVsjPcYKJOhr631AxR9XoG2rLF3QiTk5aMv36MXOjtWvm8axwNFAtKUPGsWUwLXXAMgYM1En7fsvndaXeXRQ==} - workbox-range-requests@7.3.0: - resolution: {integrity: sha512-EyFmM1KpDzzAouNF3+EWa15yDEenwxoeXu9bgxOEYnFfCxns7eAxA9WSSaVd8kujFFt3eIbShNqa4hLQNFvmVQ==} + workbox-precaching@7.4.1: + resolution: {integrity: sha512-cdr/9qByww7yzEp7zg/qI4ukUrrNjQLgN+ONQRpjy/VqGQXwkgHwr00KksGJK8v0VifwDXBb8a4cWNZH71jn3Q==} - workbox-recipes@7.3.0: - resolution: {integrity: sha512-BJro/MpuW35I/zjZQBcoxsctgeB+kyb2JAP5EB3EYzePg8wDGoQuUdyYQS+CheTb+GhqJeWmVs3QxLI8EBP1sg==} + workbox-range-requests@7.4.1: + resolution: {integrity: sha512-7i2oxAUE82gHdAJBCAQ04JzNOdRPqzuOzGfoUyJpFSmeqBNYGPrAH8GPoPjUQTfp+NycwrD2H68VtuF8qxv0vQ==} - workbox-routing@7.3.0: - resolution: {integrity: sha512-ZUlysUVn5ZUzMOmQN3bqu+gK98vNfgX/gSTZ127izJg/pMMy4LryAthnYtjuqcjkN4HEAx1mdgxNiKJMZQM76A==} + workbox-recipes@7.4.1: + resolution: {integrity: sha512-gnbVfmV4/TtmQaM4x9AtuXhcdstJsep3XMVeztOrQVPT+R6+6DeBjGTCQ7fFCXm+4GEHUA5VEBTyi5+4gWGeog==} - workbox-strategies@7.3.0: - resolution: {integrity: sha512-tmZydug+qzDFATwX7QiEL5Hdf7FrkhjaF9db1CbB39sDmEZJg3l9ayDvPxy8Y18C3Y66Nrr9kkN1f/RlkDgllg==} + workbox-routing@7.4.1: + resolution: {integrity: sha512-yubJGErZOusuidAenaL5ypfhQOa7urxP/f8E0ws7FPb4039RiWXUWBAyUkmUoOL/BcQGen3h0J8872d51IYxtA==} - workbox-streams@7.3.0: - resolution: {integrity: sha512-SZnXucyg8x2Y61VGtDjKPO5EgPUG5NDn/v86WYHX+9ZqvAsGOytP0Jxp1bl663YUuMoXSAtsGLL+byHzEuMRpw==} + workbox-strategies@7.4.1: + resolution: {integrity: sha512-GZxpaw9NbmOelj7667uZ2kpk5BFpOGbO4X0qjwh5ls8XQ8C+Lha5LQchTiUzsTFSS+NlUpftYAyOVXvQUrcqOQ==} - workbox-sw@7.3.0: - resolution: {integrity: sha512-aCUyoAZU9IZtH05mn0ACUpyHzPs0lMeJimAYkQkBsOWiqaJLgusfDCR+yllkPkFRxWpZKF8vSvgHYeG7LwhlmA==} + workbox-streams@7.4.1: + resolution: {integrity: sha512-HWWtraKUbJknd9kgqGcpQ3G114HOPYvqs8HaJMDs2ebLNAimDkVDaWfAXE6Ybl+m8U6KsCE6pWyLYuigWmnAXw==} - workbox-window@7.3.0: - resolution: {integrity: sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA==} + workbox-sw@7.4.1: + resolution: {integrity: sha512-fez5f2DUlDJWTFYkCWQpY10N8gtztd849NswCbVFk0QlcSM4HT5A8x4g4ii650yem4I8tHY0R7JZahwp3ltIPw==} workbox-window@7.4.1: resolution: {integrity: sha512-notZDH2u8VXaqyuD7xaqIfEFi6SRM4SUSd7ewe9PDsVqADuepxX2ZMY3uvuZGxzY5ZOsGC/vD3A/3smFtJt4/A==} @@ -3898,9 +4126,6 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - xxhash-wasm@1.1.0: resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} @@ -3955,36 +4180,36 @@ snapshots: jsonpointer: 5.0.1 leven: 3.1.0 - '@astrojs/check@0.9.9(prettier@3.9.4)(typescript@6.0.3)': + '@astrojs/check@0.9.9(prettier@3.9.5)(typescript@7.0.2)': dependencies: - '@astrojs/language-server': 2.16.11(prettier@3.9.4)(typescript@6.0.3) + '@astrojs/language-server': 2.16.11(prettier@3.9.5)(typescript@7.0.2) chokidar: 4.0.3 kleur: 4.1.5 - typescript: 6.0.3 + typescript: 7.0.2 yargs: 17.7.3 transitivePeerDependencies: - prettier - prettier-plugin-astro - '@astrojs/compiler-binding-darwin-arm64@0.3.0': + '@astrojs/compiler-binding-darwin-arm64@0.3.1': optional: true - '@astrojs/compiler-binding-darwin-x64@0.3.0': + '@astrojs/compiler-binding-darwin-x64@0.3.1': optional: true - '@astrojs/compiler-binding-linux-arm64-gnu@0.3.0': + '@astrojs/compiler-binding-linux-arm64-gnu@0.3.1': optional: true - '@astrojs/compiler-binding-linux-arm64-musl@0.3.0': + '@astrojs/compiler-binding-linux-arm64-musl@0.3.1': optional: true - '@astrojs/compiler-binding-linux-x64-gnu@0.3.0': + '@astrojs/compiler-binding-linux-x64-gnu@0.3.1': optional: true - '@astrojs/compiler-binding-linux-x64-musl@0.3.0': + '@astrojs/compiler-binding-linux-x64-musl@0.3.1': optional: true - '@astrojs/compiler-binding-wasm32-wasi@0.3.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)': + '@astrojs/compiler-binding-wasm32-wasi@0.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)': dependencies: '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2) transitivePeerDependencies: @@ -3992,30 +4217,30 @@ snapshots: - '@emnapi/runtime' optional: true - '@astrojs/compiler-binding-win32-arm64-msvc@0.3.0': + '@astrojs/compiler-binding-win32-arm64-msvc@0.3.1': optional: true - '@astrojs/compiler-binding-win32-x64-msvc@0.3.0': + '@astrojs/compiler-binding-win32-x64-msvc@0.3.1': optional: true - '@astrojs/compiler-binding@0.3.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)': + '@astrojs/compiler-binding@0.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)': optionalDependencies: - '@astrojs/compiler-binding-darwin-arm64': 0.3.0 - '@astrojs/compiler-binding-darwin-x64': 0.3.0 - '@astrojs/compiler-binding-linux-arm64-gnu': 0.3.0 - '@astrojs/compiler-binding-linux-arm64-musl': 0.3.0 - '@astrojs/compiler-binding-linux-x64-gnu': 0.3.0 - '@astrojs/compiler-binding-linux-x64-musl': 0.3.0 - '@astrojs/compiler-binding-wasm32-wasi': 0.3.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2) - '@astrojs/compiler-binding-win32-arm64-msvc': 0.3.0 - '@astrojs/compiler-binding-win32-x64-msvc': 0.3.0 + '@astrojs/compiler-binding-darwin-arm64': 0.3.1 + '@astrojs/compiler-binding-darwin-x64': 0.3.1 + '@astrojs/compiler-binding-linux-arm64-gnu': 0.3.1 + '@astrojs/compiler-binding-linux-arm64-musl': 0.3.1 + '@astrojs/compiler-binding-linux-x64-gnu': 0.3.1 + '@astrojs/compiler-binding-linux-x64-musl': 0.3.1 + '@astrojs/compiler-binding-wasm32-wasi': 0.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2) + '@astrojs/compiler-binding-win32-arm64-msvc': 0.3.1 + '@astrojs/compiler-binding-win32-x64-msvc': 0.3.1 transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' - '@astrojs/compiler-rs@0.3.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)': + '@astrojs/compiler-rs@0.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)': dependencies: - '@astrojs/compiler-binding': 0.3.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2) + '@astrojs/compiler-binding': 0.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -4027,18 +4252,18 @@ snapshots: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 js-yaml: 4.3.0 - picomatch: 4.0.4 + picomatch: 4.0.5 retext-smartypants: 6.2.0 - shiki: 4.3.0 + shiki: 4.3.1 smol-toml: 1.7.0 unified: 11.0.5 - '@astrojs/language-server@2.16.11(prettier@3.9.4)(typescript@6.0.3)': + '@astrojs/language-server@2.16.11(prettier@3.9.5)(typescript@7.0.2)': dependencies: '@astrojs/compiler': 2.13.1 '@astrojs/yaml2ts': 0.2.4 '@jridgewell/sourcemap-codec': 1.5.5 - '@volar/kit': 2.4.28(typescript@6.0.3) + '@volar/kit': 2.4.28(typescript@7.0.2) '@volar/language-core': 2.4.28 '@volar/language-server': 2.4.28 '@volar/language-service': 2.4.28 @@ -4047,14 +4272,14 @@ snapshots: volar-service-css: 0.0.71(@volar/language-service@2.4.28) volar-service-emmet: 0.0.71(@volar/language-service@2.4.28) volar-service-html: 0.0.71(@volar/language-service@2.4.28) - volar-service-prettier: 0.0.71(@volar/language-service@2.4.28)(prettier@3.9.4) + volar-service-prettier: 0.0.71(@volar/language-service@2.4.28)(prettier@3.9.5) volar-service-typescript: 0.0.71(@volar/language-service@2.4.28) volar-service-typescript-twoslash-queries: 0.0.71(@volar/language-service@2.4.28) volar-service-yaml: 0.0.71(@volar/language-service@2.4.28) vscode-html-languageservice: 5.6.2 vscode-uri: 3.1.0 optionalDependencies: - prettier: 3.9.4 + prettier: 3.9.5 transitivePeerDependencies: - typescript @@ -4085,15 +4310,15 @@ snapshots: '@astrojs/internal-helpers': 0.10.1 '@astrojs/prism': 4.0.2 github-slugger: 2.0.0 - satteri: 0.9.4 + satteri: 0.9.5 - '@astrojs/mdx@7.0.2(@astrojs/markdown-satteri@0.3.3)(astro@7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': + '@astrojs/mdx@7.0.2(@astrojs/markdown-satteri@0.3.3)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: '@astrojs/internal-helpers': 0.10.1 '@astrojs/markdown-remark': 7.2.1 '@mdx-js/mdx': 3.1.1 acorn: 8.17.0 - astro: 7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + astro: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) es-module-lexer: 2.3.0 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 @@ -4118,12 +4343,12 @@ snapshots: '@astrojs/internal-helpers': 0.10.1 '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@vitejs/plugin-react': 5.2.0(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + '@vitejs/plugin-react': 5.2.0(vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) devalue: 5.8.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - ultrahtml: 1.6.0 - vite: 8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + ultrahtml: 1.7.0 + vite: 8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - '@vitejs/devtools' @@ -4145,23 +4370,23 @@ snapshots: stream-replace-string: 2.0.0 zod: 4.4.3 - '@astrojs/starlight@0.41.3(@astrojs/markdown-remark@7.2.1)(astro@7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3)': + '@astrojs/starlight@0.41.3(@astrojs/markdown-remark@7.2.1)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@7.0.2)': dependencies: '@astrojs/markdown-satteri': 0.3.3 - '@astrojs/mdx': 7.0.2(@astrojs/markdown-satteri@0.3.3)(astro@7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + '@astrojs/mdx': 7.0.2(@astrojs/markdown-satteri@0.3.3)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) '@astrojs/sitemap': 3.7.3 '@pagefind/default-ui': 1.5.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/js-yaml': 4.0.9 '@types/mdast': 4.0.4 - astro: 7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - astro-expressive-code: 0.44.0(astro@7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + astro: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + astro-expressive-code: 0.44.0(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) bcp-47: 2.1.1 hast-util-from-html: 2.0.3 hast-util-select: 6.0.4 hast-util-to-string: 3.0.1 hastscript: 9.0.1 - i18next: 26.3.4(typescript@6.0.3) + i18next: 26.3.6(typescript@7.0.2) js-yaml: 4.3.0 klona: 2.0.6 magic-string: 0.30.21 @@ -4172,8 +4397,8 @@ snapshots: rehype: 13.0.2 rehype-format: 5.0.1 remark-directive: 4.0.0 - satteri: 0.9.4 - ultrahtml: 1.6.0 + satteri: 0.9.5 + ultrahtml: 1.7.0 unified: 11.0.5 unist-util-visit: 5.1.0 vfile: 6.0.3 @@ -4183,13 +4408,12 @@ snapshots: - supports-color - typescript - '@astrojs/telemetry@3.3.2': + '@astrojs/telemetry@3.3.3': dependencies: ci-info: 4.4.0 dset: 3.1.4 is-docker: 4.0.0 - is-wsl: 3.1.1 - which-pm-runs: 1.1.0 + package-manager-detector: 1.7.0 '@astrojs/yaml2ts@0.2.4': dependencies: @@ -4868,35 +5092,35 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@bruits/satteri-darwin-arm64@0.9.4': + '@bruits/satteri-darwin-arm64@0.9.5': optional: true - '@bruits/satteri-darwin-x64@0.9.4': + '@bruits/satteri-darwin-x64@0.9.5': optional: true - '@bruits/satteri-linux-arm64-gnu@0.9.4': + '@bruits/satteri-linux-arm64-gnu@0.9.5': optional: true - '@bruits/satteri-linux-arm64-musl@0.9.4': + '@bruits/satteri-linux-arm64-musl@0.9.5': optional: true - '@bruits/satteri-linux-x64-gnu@0.9.4': + '@bruits/satteri-linux-x64-gnu@0.9.5': optional: true - '@bruits/satteri-linux-x64-musl@0.9.4': + '@bruits/satteri-linux-x64-musl@0.9.5': optional: true - '@bruits/satteri-wasm32-wasi@0.9.4': + '@bruits/satteri-wasm32-wasi@0.9.5': dependencies: '@emnapi/core': 1.11.1 '@emnapi/runtime': 1.11.1 '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@bruits/satteri-win32-arm64-msvc@0.9.4': + '@bruits/satteri-win32-arm64-msvc@0.9.5': optional: true - '@bruits/satteri-win32-x64-msvc@0.9.4': + '@bruits/satteri-win32-x64-msvc@0.9.5': optional: true '@capsizecss/unpack@4.0.1': @@ -5170,6 +5394,8 @@ snapshots: '@img/sharp-win32-x64@0.35.3': optional: true + '@isaacs/cliui@9.0.0': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -5200,7 +5426,7 @@ snapshots: dependencies: '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdx': 2.0.14 acorn: 8.17.0 collapse-white-space: 2.1.0 @@ -5242,7 +5468,7 @@ snapshots: '@oslojs/encoding@1.1.0': {} - '@oxc-project/types@0.137.0': {} + '@oxc-project/types@0.139.0': {} '@pagefind/darwin-arm64@1.5.2': optional: true @@ -5333,192 +5559,223 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@rolldown/binding-android-arm64@1.1.3': + '@rolldown/binding-android-arm64@1.1.5': optional: true - '@rolldown/binding-darwin-arm64@1.1.3': + '@rolldown/binding-darwin-arm64@1.1.5': optional: true - '@rolldown/binding-darwin-x64@1.1.3': + '@rolldown/binding-darwin-x64@1.1.5': optional: true - '@rolldown/binding-freebsd-x64@1.1.3': + '@rolldown/binding-freebsd-x64@1.1.5': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.3': + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.3': + '@rolldown/binding-linux-arm64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-arm64-musl@1.1.3': + '@rolldown/binding-linux-arm64-musl@1.1.5': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.3': + '@rolldown/binding-linux-ppc64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.3': + '@rolldown/binding-linux-s390x-gnu@1.1.5': optional: true - '@rolldown/binding-linux-x64-gnu@1.1.3': + '@rolldown/binding-linux-x64-gnu@1.1.5': optional: true - '@rolldown/binding-linux-x64-musl@1.1.3': + '@rolldown/binding-linux-x64-musl@1.1.5': optional: true - '@rolldown/binding-openharmony-arm64@1.1.3': + '@rolldown/binding-openharmony-arm64@1.1.5': optional: true - '@rolldown/binding-wasm32-wasi@1.1.3': + '@rolldown/binding-wasm32-wasi@1.1.5': dependencies: '@emnapi/core': 1.11.1 '@emnapi/runtime': 1.11.1 '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.3': + '@rolldown/binding-win32-arm64-msvc@1.1.5': optional: true - '@rolldown/binding-win32-x64-msvc@1.1.3': + '@rolldown/binding-win32-x64-msvc@1.1.5': optional: true '@rolldown/pluginutils@1.0.0-rc.3': {} '@rolldown/pluginutils@1.0.1': {} - '@rollup/plugin-babel@5.3.1(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@2.80.0)': + '@rollup/plugin-babel@6.1.0(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@4.62.2)': dependencies: '@babel/core': 7.29.7 '@babel/helper-module-imports': 7.29.7 - '@rollup/pluginutils': 3.1.0(rollup@2.80.0) - rollup: 2.80.0 + '@rollup/pluginutils': 5.4.0(rollup@4.62.2) optionalDependencies: '@types/babel__core': 7.20.5 + rollup: 4.62.2 transitivePeerDependencies: - supports-color - '@rollup/plugin-node-resolve@15.3.1(rollup@2.80.0)': + '@rollup/plugin-node-resolve@16.0.3(rollup@4.62.2)': dependencies: - '@rollup/pluginutils': 5.4.0(rollup@2.80.0) + '@rollup/pluginutils': 5.4.0(rollup@4.62.2) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 resolve: 1.22.12 optionalDependencies: - rollup: 2.80.0 + rollup: 4.62.2 - '@rollup/plugin-replace@2.4.2(rollup@2.80.0)': + '@rollup/plugin-replace@6.0.3(rollup@4.62.2)': dependencies: - '@rollup/pluginutils': 3.1.0(rollup@2.80.0) - magic-string: 0.25.9 - rollup: 2.80.0 + '@rollup/pluginutils': 5.4.0(rollup@4.62.2) + magic-string: 0.30.21 + optionalDependencies: + rollup: 4.62.2 - '@rollup/plugin-terser@0.4.4(rollup@2.80.0)': + '@rollup/plugin-terser@1.0.0(rollup@4.62.2)': dependencies: - serialize-javascript: 6.0.2 + serialize-javascript: 7.0.7 smob: 1.6.2 terser: 5.48.0 optionalDependencies: - rollup: 2.80.0 + rollup: 4.62.2 - '@rollup/pluginutils@3.1.0(rollup@2.80.0)': - dependencies: - '@types/estree': 0.0.39 - estree-walker: 1.0.1 - picomatch: 2.3.2 - rollup: 2.80.0 - - '@rollup/pluginutils@5.4.0(rollup@2.80.0)': + '@rollup/pluginutils@5.4.0(rollup@4.62.2)': dependencies: '@types/estree': 1.0.9 estree-walker: 2.0.2 picomatch: 4.0.5 optionalDependencies: - rollup: 2.80.0 + rollup: 4.62.2 - '@shikijs/core@4.3.0': - dependencies: - '@shikijs/primitive': 4.3.0 - '@shikijs/types': 4.3.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true '@shikijs/core@4.3.1': dependencies: '@shikijs/primitive': 4.3.1 '@shikijs/types': 4.3.1 '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@4.3.0': - dependencies: - '@shikijs/types': 4.3.0 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.6 - '@shikijs/engine-javascript@4.3.1': dependencies: '@shikijs/types': 4.3.1 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.6 - '@shikijs/engine-oniguruma@4.3.0': - dependencies: - '@shikijs/types': 4.3.0 - '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/engine-oniguruma@4.3.1': dependencies: '@shikijs/types': 4.3.1 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@4.3.0': - dependencies: - '@shikijs/types': 4.3.0 - '@shikijs/langs@4.3.1': dependencies: '@shikijs/types': 4.3.1 - '@shikijs/primitive@4.3.0': - dependencies: - '@shikijs/types': 4.3.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - '@shikijs/primitive@4.3.1': dependencies: '@shikijs/types': 4.3.1 '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - - '@shikijs/themes@4.3.0': - dependencies: - '@shikijs/types': 4.3.0 + '@types/hast': 3.0.5 '@shikijs/themes@4.3.1': dependencies: '@shikijs/types': 4.3.1 - '@shikijs/types@4.3.0': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - '@shikijs/types@4.3.1': dependencies: '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@shikijs/vscode-textmate@10.0.2': {} - '@surma/rollup-plugin-off-main-thread@2.2.3': + '@trickfilm400/rollup-plugin-off-main-thread@3.0.0-pre1': dependencies: ejs: 3.1.10 json5: 2.2.3 - magic-string: 0.25.9 + magic-string: 0.30.21 string.prototype.matchall: 4.0.12 '@tybys/wasm-util@0.10.3': @@ -5555,14 +5812,16 @@ snapshots: dependencies: '@types/estree': 1.0.9 - '@types/estree@0.0.39': {} - '@types/estree@1.0.9': {} '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + '@types/js-yaml@4.0.9': {} '@types/mdast@4.0.4': @@ -5577,13 +5836,14 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/node@24.13.2': + '@types/node@24.13.3': dependencies: undici-types: 7.18.2 '@types/node@26.1.0': dependencies: undici-types: 8.3.0 + optional: true '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: @@ -5597,7 +5857,7 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 26.1.0 + '@types/node': 24.13.3 '@types/trusted-types@2.0.7': {} @@ -5605,14 +5865,74 @@ snapshots: '@types/unist@3.0.3': {} + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true + + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true + + '@typescript/typescript-darwin-x64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm@7.0.2': + optional: true + + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true + '@ungap/structured-clone@1.3.2': {} - '@vite-pwa/astro@1.2.0(astro@7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1))': + '@vite-pwa/astro@1.2.0(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1))': dependencies: - astro: 7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - vite-plugin-pwa: 1.3.0(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1) + astro: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite-plugin-pwa: 1.3.0(vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1) - '@vitejs/plugin-react@5.2.0(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': + '@vitejs/plugin-react@5.2.0(vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) @@ -5620,16 +5940,16 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color - '@volar/kit@2.4.28(typescript@6.0.3)': + '@volar/kit@2.4.28(typescript@7.0.2)': dependencies: '@volar/language-service': 2.4.28 '@volar/typescript': 2.4.28 typesafe-path: 0.2.2 - typescript: 6.0.3 + typescript: 7.0.2 vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.1.0 @@ -5735,22 +6055,22 @@ snapshots: astring@1.9.0: {} - astro-expressive-code@0.44.0(astro@7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): + astro-expressive-code@0.44.0(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): dependencies: - astro: 7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + astro: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) rehype-expressive-code: 0.44.0 url-extras: 0.1.0 - astro@7.0.6(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@2.80.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): + astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): dependencies: - '@astrojs/compiler-rs': 0.3.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2) + '@astrojs/compiler-rs': 0.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2) '@astrojs/internal-helpers': 0.10.1 '@astrojs/markdown-satteri': 0.3.3 - '@astrojs/telemetry': 3.3.2 + '@astrojs/telemetry': 3.3.3 '@capsizecss/unpack': 4.0.1 '@clack/prompts': 1.7.0 '@oslojs/encoding': 1.1.0 - '@rollup/pluginutils': 5.4.0(rollup@2.80.0) + '@rollup/pluginutils': 5.4.0(rollup@4.62.2) am-i-vibing: 0.4.0 aria-query: 5.3.2 axobject-query: 4.1.0 @@ -5788,11 +6108,11 @@ snapshots: tinyclip: 0.1.15 tinyexec: 1.2.4 tinyglobby: 0.2.17 - ultrahtml: 1.6.0 + ultrahtml: 1.7.0 unifont: 0.7.4 unstorage: 1.17.5 - vite: 8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - vitefu: 1.1.3(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + vite: 8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vitefu: 1.1.3(vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) xxhash-wasm: 1.1.0 yargs-parser: 22.0.0 zod: 4.4.3 @@ -5874,6 +6194,8 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + baseline-browser-mapping@2.10.40: {} bcp-47-match@2.0.3: {} @@ -5886,14 +6208,13 @@ snapshots: boolbase@1.0.0: {} - brace-expansion@1.1.15: + brace-expansion@2.1.1: dependencies: balanced-match: 1.0.2 - concat-map: 0.0.1 - brace-expansion@2.1.1: + brace-expansion@5.0.7: dependencies: - balanced-match: 1.0.2 + balanced-match: 4.0.4 browserslist@4.28.4: dependencies: @@ -5972,8 +6293,6 @@ snapshots: common-tags@1.8.2: {} - concat-map@0.0.1: {} - convert-source-map@2.0.0: {} cookie-es@1.2.3: {} @@ -5984,6 +6303,12 @@ snapshots: dependencies: browserslist: 4.28.4 + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + crossws@0.3.5: dependencies: uncrypto: 0.1.3 @@ -6287,8 +6612,6 @@ snapshots: '@types/estree-jsx': 1.0.5 '@types/unist': 3.0.3 - estree-walker@1.0.1: {} - estree-walker@2.0.2: {} estree-walker@3.0.3: @@ -6297,6 +6620,8 @@ snapshots: esutils@2.0.3: {} + eta@4.6.0: {} + eventemitter3@5.0.4: {} expressive-code@0.44.0: @@ -6346,6 +6671,11 @@ snapshots: dependencies: is-callable: 1.2.7 + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + fs-extra@9.1.0: dependencies: at-least-node: 1.0.0 @@ -6353,8 +6683,6 @@ snapshots: jsonfile: 6.2.1 universalify: 2.0.1 - fs.realpath@1.0.0: {} - fsevents@2.3.3: optional: true @@ -6412,14 +6740,14 @@ snapshots: github-slugger@2.0.0: {} - glob@7.2.3: + glob@11.1.0: dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.5 - once: 1.4.0 - path-is-absolute: 1.0.1 + foreground-child: 3.3.1 + jackspeak: 4.2.3 + minimatch: 10.2.5 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.2 globalthis@1.0.4: dependencies: @@ -6464,12 +6792,12 @@ snapshots: hast-util-embedded@3.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-is-element: 3.0.0 hast-util-format@1.1.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-embedded: 3.0.0 hast-util-minify-whitespace: 1.0.1 hast-util-phrasing: 3.0.1 @@ -6499,11 +6827,11 @@ snapshots: hast-util-has-property@3.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-is-body-ok-link@3.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-is-element@3.0.0: dependencies: @@ -6511,7 +6839,7 @@ snapshots: hast-util-minify-whitespace@1.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-embedded: 3.0.0 hast-util-is-element: 3.0.0 hast-util-whitespace: 3.0.0 @@ -6519,11 +6847,11 @@ snapshots: hast-util-parse-selector@4.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-phrasing@3.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-embedded: 3.0.0 hast-util-has-property: 3.0.0 hast-util-is-body-ok-link: 3.0.1 @@ -6547,7 +6875,7 @@ snapshots: hast-util-select@6.0.4: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 bcp-47-match: 2.0.3 comma-separated-tokens: 2.0.3 @@ -6567,7 +6895,7 @@ snapshots: dependencies: '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 comma-separated-tokens: 2.0.3 devlop: 1.1.0 estree-util-attach-comments: 3.0.0 @@ -6601,7 +6929,7 @@ snapshots: hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.9 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 comma-separated-tokens: 2.0.3 devlop: 1.1.0 @@ -6630,7 +6958,7 @@ snapshots: hast-util-to-string@3.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-to-text@4.0.2: dependencies: @@ -6641,11 +6969,11 @@ snapshots: hast-util-whitespace@3.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hastscript@9.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 comma-separated-tokens: 2.0.3 hast-util-parse-selector: 4.0.0 property-information: 7.2.0 @@ -6659,21 +6987,14 @@ snapshots: http-cache-semantics@4.2.0: {} - i18next@26.3.4(typescript@6.0.3): + i18next@26.3.6(typescript@7.0.2): optionalDependencies: - typescript: 6.0.3 + typescript: 7.0.2 idb@7.1.1: {} immutable@5.1.9: {} - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - inline-style-parser@0.2.7: {} internal-slot@1.1.0: @@ -6733,8 +7054,6 @@ snapshots: is-decimal@2.0.1: {} - is-docker@3.0.0: {} - is-docker@4.0.0: {} is-document.all@1.0.0: @@ -6765,10 +7084,6 @@ snapshots: is-hexadecimal@2.0.1: {} - is-inside-container@1.0.0: - dependencies: - is-docker: 3.0.0 - is-map@2.0.3: {} is-module@1.0.0: {} @@ -6827,12 +7142,14 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 - is-wsl@3.1.1: - dependencies: - is-inside-container: 1.0.0 - isarray@2.0.5: {} + isexe@2.0.0: {} + + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + jake@10.9.4: dependencies: async: 3.2.6 @@ -6926,20 +7243,16 @@ snapshots: lodash.sortby@4.7.0: {} - lodash@4.18.1: {} - longest-streak@3.1.0: {} lru-cache@11.5.1: {} + lru-cache@11.5.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 - magic-string@0.25.9: - dependencies: - sourcemap-codec: 1.4.8 - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -7060,7 +7373,7 @@ snapshots: mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 mdast-util-from-markdown: 2.0.3 @@ -7071,7 +7384,7 @@ snapshots: mdast-util-mdx-jsx@3.2.0: dependencies: '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 '@types/unist': 3.0.3 ccount: 2.0.1 @@ -7098,7 +7411,7 @@ snapshots: mdast-util-mdxjs-esm@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 mdast-util-from-markdown: 2.0.3 @@ -7417,14 +7730,16 @@ snapshots: transitivePeerDependencies: - supports-color - minimatch@3.1.5: + minimatch@10.2.5: dependencies: - brace-expansion: 1.1.15 + brace-expansion: 5.0.7 minimatch@5.1.9: dependencies: brace-expansion: 2.1.1 + minipass@7.1.3: {} + mrmime@2.0.1: {} ms@2.1.3: {} @@ -7477,10 +7792,6 @@ snapshots: ohash@2.0.11: {} - once@1.4.0: - dependencies: - wrappy: 1.0.2 - oniguruma-parser@0.12.2: {} oniguruma-to-es@4.3.6: @@ -7506,6 +7817,8 @@ snapshots: p-timeout@7.0.1: {} + package-json-from-dist@1.0.1: {} + package-manager-detector@1.7.0: {} pagefind@1.5.2: @@ -7543,18 +7856,21 @@ snapshots: path-browserify@1.0.1: {} - path-is-absolute@1.0.1: {} + path-key@3.1.1: {} path-parse@1.0.7: {} + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.1 + minipass: 7.1.3 + piccolore@0.1.3: {} picocolors@1.1.1: {} picomatch@2.3.2: {} - picomatch@4.0.4: {} - picomatch@4.0.5: {} possible-typed-array-names@1.1.0: {} @@ -7575,7 +7891,7 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prettier@3.9.4: {} + prettier@3.9.5: {} pretty-bytes@5.6.0: {} @@ -7591,10 +7907,6 @@ snapshots: radix3@1.1.2: {} - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - react-dom@19.2.7(react@19.2.7): dependencies: react: 19.2.7 @@ -7694,12 +8006,12 @@ snapshots: rehype-format@5.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-format: 1.1.0 rehype-parse@9.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-from-html: 2.0.3 unified: 11.0.5 @@ -7712,7 +8024,7 @@ snapshots: rehype-recma@1.0.0: dependencies: '@types/estree': 1.0.9 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-to-estree: 3.1.3 transitivePeerDependencies: - supports-color @@ -7725,7 +8037,7 @@ snapshots: rehype@13.0.2: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 rehype-parse: 9.0.1 rehype-stringify: 10.0.1 unified: 11.0.5 @@ -7829,29 +8141,56 @@ snapshots: retext-stringify: 4.0.0 unified: 11.0.5 - rolldown@1.1.3: + rolldown@1.1.5: dependencies: - '@oxc-project/types': 0.137.0 + '@oxc-project/types': 0.139.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.3 - '@rolldown/binding-darwin-arm64': 1.1.3 - '@rolldown/binding-darwin-x64': 1.1.3 - '@rolldown/binding-freebsd-x64': 1.1.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.3 - '@rolldown/binding-linux-arm64-gnu': 1.1.3 - '@rolldown/binding-linux-arm64-musl': 1.1.3 - '@rolldown/binding-linux-ppc64-gnu': 1.1.3 - '@rolldown/binding-linux-s390x-gnu': 1.1.3 - '@rolldown/binding-linux-x64-gnu': 1.1.3 - '@rolldown/binding-linux-x64-musl': 1.1.3 - '@rolldown/binding-openharmony-arm64': 1.1.3 - '@rolldown/binding-wasm32-wasi': 1.1.3 - '@rolldown/binding-win32-arm64-msvc': 1.1.3 - '@rolldown/binding-win32-x64-msvc': 1.1.3 - - rollup@2.80.0: + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 + + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 fsevents: 2.3.3 safe-array-concat@1.1.4: @@ -7862,8 +8201,6 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 - safe-buffer@5.2.1: {} - safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -7883,22 +8220,22 @@ snapshots: optionalDependencies: '@parcel/watcher': 2.5.6 - satteri@0.9.4: + satteri@0.9.5: dependencies: '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 '@types/unist': 3.0.3 optionalDependencies: - '@bruits/satteri-darwin-arm64': 0.9.4 - '@bruits/satteri-darwin-x64': 0.9.4 - '@bruits/satteri-linux-arm64-gnu': 0.9.4 - '@bruits/satteri-linux-arm64-musl': 0.9.4 - '@bruits/satteri-linux-x64-gnu': 0.9.4 - '@bruits/satteri-linux-x64-musl': 0.9.4 - '@bruits/satteri-wasm32-wasi': 0.9.4 - '@bruits/satteri-win32-arm64-msvc': 0.9.4 - '@bruits/satteri-win32-x64-msvc': 0.9.4 + '@bruits/satteri-darwin-arm64': 0.9.5 + '@bruits/satteri-darwin-x64': 0.9.5 + '@bruits/satteri-linux-arm64-gnu': 0.9.5 + '@bruits/satteri-linux-arm64-musl': 0.9.5 + '@bruits/satteri-linux-x64-gnu': 0.9.5 + '@bruits/satteri-linux-x64-musl': 0.9.5 + '@bruits/satteri-wasm32-wasi': 0.9.5 + '@bruits/satteri-win32-arm64-msvc': 0.9.5 + '@bruits/satteri-win32-x64-msvc': 0.9.5 sax@1.6.0: {} @@ -7908,9 +8245,7 @@ snapshots: semver@7.8.5: {} - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 + serialize-javascript@7.0.7: {} set-function-length@1.2.2: dependencies: @@ -7967,16 +8302,11 @@ snapshots: '@img/sharp-win32-x64': 0.35.3 '@types/node': 26.1.0 - shiki@4.3.0: + shebang-command@2.0.0: dependencies: - '@shikijs/core': 4.3.0 - '@shikijs/engine-javascript': 4.3.0 - '@shikijs/engine-oniguruma': 4.3.0 - '@shikijs/langs': 4.3.0 - '@shikijs/themes': 4.3.0 - '@shikijs/types': 4.3.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} shiki@4.3.1: dependencies: @@ -7987,7 +8317,7 @@ snapshots: '@shikijs/themes': 4.3.1 '@shikijs/types': 4.3.1 '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 side-channel-list@1.0.1: dependencies: @@ -8017,11 +8347,13 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + signal-exit@4.1.0: {} + sisteransi@1.0.5: {} sitemap@9.0.1: dependencies: - '@types/node': 24.13.2 + '@types/node': 24.13.3 '@types/sax': 1.2.7 arg: 5.0.2 sax: 1.6.0 @@ -8045,8 +8377,6 @@ snapshots: dependencies: whatwg-url: 7.1.0 - sourcemap-codec@1.4.8: {} - space-separated-tokens@2.0.2: {} stop-iteration-iterator@1.1.0: @@ -8218,11 +8548,32 @@ snapshots: dependencies: semver: 7.8.5 - typescript@6.0.3: {} + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 ufo@1.6.4: {} - ultrahtml@1.6.0: {} + ultrahtml@1.7.0: {} unbox-primitive@1.1.0: dependencies: @@ -8235,7 +8586,8 @@ snapshots: undici-types@7.18.2: {} - undici-types@8.3.0: {} + undici-types@8.3.0: + optional: true unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -8322,7 +8674,7 @@ snapshots: chokidar: 5.0.0 destr: 2.0.5 h3: 1.15.11 - lru-cache: 11.5.1 + lru-cache: 11.5.2 node-fetch-native: 1.6.7 ofetch: 1.5.1 ufo: 1.6.4 @@ -8354,23 +8706,23 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-pwa@1.3.0(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.4.1): + vite-plugin-pwa@1.3.0(vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1): dependencies: debug: 4.4.3 pretty-bytes: 6.1.1 tinyglobby: 0.2.17 - vite: 8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - workbox-build: 7.3.0(@types/babel__core@7.20.5) + vite: 8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + workbox-build: 7.4.1(@types/babel__core@7.20.5) workbox-window: 7.4.1 transitivePeerDependencies: - supports-color - vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): + vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 - picomatch: 4.0.4 + picomatch: 4.0.5 postcss: 8.5.16 - rolldown: 1.1.3 + rolldown: 1.1.5 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 26.1.0 @@ -8380,9 +8732,9 @@ snapshots: terser: 5.48.0 yaml: 2.9.0 - vitefu@1.1.3(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): + vitefu@1.1.3(vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): optionalDependencies: - vite: 8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) volar-service-css@0.0.71(@volar/language-service@2.4.28): dependencies: @@ -8409,12 +8761,12 @@ snapshots: optionalDependencies: '@volar/language-service': 2.4.28 - volar-service-prettier@0.0.71(@volar/language-service@2.4.28)(prettier@3.9.4): + volar-service-prettier@0.0.71(@volar/language-service@2.4.28)(prettier@3.9.5): dependencies: vscode-uri: 3.1.0 optionalDependencies: '@volar/language-service': 2.4.28 - prettier: 3.9.4 + prettier: 3.9.5 volar-service-typescript-twoslash-queries@0.0.71(@volar/language-service@2.4.28): dependencies: @@ -8531,8 +8883,6 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 - which-pm-runs@1.1.0: {} - which-typed-array@1.1.22: dependencies: available-typed-arrays: 1.0.7 @@ -8543,120 +8893,117 @@ snapshots: gopd: 1.2.0 has-tostringtag: 1.0.2 - workbox-background-sync@7.3.0: + which@2.0.2: + dependencies: + isexe: 2.0.0 + + workbox-background-sync@7.4.1: dependencies: idb: 7.1.1 - workbox-core: 7.3.0 + workbox-core: 7.4.1 - workbox-broadcast-update@7.3.0: + workbox-broadcast-update@7.4.1: dependencies: - workbox-core: 7.3.0 + workbox-core: 7.4.1 - workbox-build@7.3.0(@types/babel__core@7.20.5): + workbox-build@7.4.1(@types/babel__core@7.20.5): dependencies: '@apideck/better-ajv-errors': 0.3.7(ajv@8.20.0) '@babel/core': 7.29.7 '@babel/preset-env': 7.29.7(@babel/core@7.29.7) '@babel/runtime': 7.29.7 - '@rollup/plugin-babel': 5.3.1(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@2.80.0) - '@rollup/plugin-node-resolve': 15.3.1(rollup@2.80.0) - '@rollup/plugin-replace': 2.4.2(rollup@2.80.0) - '@rollup/plugin-terser': 0.4.4(rollup@2.80.0) - '@surma/rollup-plugin-off-main-thread': 2.2.3 + '@rollup/plugin-babel': 6.1.0(@babel/core@7.29.7)(@types/babel__core@7.20.5)(rollup@4.62.2) + '@rollup/plugin-node-resolve': 16.0.3(rollup@4.62.2) + '@rollup/plugin-replace': 6.0.3(rollup@4.62.2) + '@rollup/plugin-terser': 1.0.0(rollup@4.62.2) + '@trickfilm400/rollup-plugin-off-main-thread': 3.0.0-pre1 ajv: 8.20.0 common-tags: 1.8.2 + eta: 4.6.0 fast-json-stable-stringify: 2.1.0 fs-extra: 9.1.0 - glob: 7.2.3 - lodash: 4.18.1 + glob: 11.1.0 pretty-bytes: 5.6.0 - rollup: 2.80.0 + rollup: 4.62.2 source-map: 0.8.0-beta.0 stringify-object: 3.3.0 strip-comments: 2.0.1 tempy: 0.6.0 upath: 1.2.0 - workbox-background-sync: 7.3.0 - workbox-broadcast-update: 7.3.0 - workbox-cacheable-response: 7.3.0 - workbox-core: 7.3.0 - workbox-expiration: 7.3.0 - workbox-google-analytics: 7.3.0 - workbox-navigation-preload: 7.3.0 - workbox-precaching: 7.3.0 - workbox-range-requests: 7.3.0 - workbox-recipes: 7.3.0 - workbox-routing: 7.3.0 - workbox-strategies: 7.3.0 - workbox-streams: 7.3.0 - workbox-sw: 7.3.0 - workbox-window: 7.3.0 + workbox-background-sync: 7.4.1 + workbox-broadcast-update: 7.4.1 + workbox-cacheable-response: 7.4.1 + workbox-core: 7.4.1 + workbox-expiration: 7.4.1 + workbox-google-analytics: 7.4.1 + workbox-navigation-preload: 7.4.1 + workbox-precaching: 7.4.1 + workbox-range-requests: 7.4.1 + workbox-recipes: 7.4.1 + workbox-routing: 7.4.1 + workbox-strategies: 7.4.1 + workbox-streams: 7.4.1 + workbox-sw: 7.4.1 + workbox-window: 7.4.1 transitivePeerDependencies: - '@types/babel__core' - supports-color - workbox-cacheable-response@7.3.0: + workbox-cacheable-response@7.4.1: dependencies: - workbox-core: 7.3.0 - - workbox-core@7.3.0: {} + workbox-core: 7.4.1 workbox-core@7.4.1: {} - workbox-expiration@7.3.0: + workbox-expiration@7.4.1: dependencies: idb: 7.1.1 - workbox-core: 7.3.0 + workbox-core: 7.4.1 - workbox-google-analytics@7.3.0: + workbox-google-analytics@7.4.1: dependencies: - workbox-background-sync: 7.3.0 - workbox-core: 7.3.0 - workbox-routing: 7.3.0 - workbox-strategies: 7.3.0 + workbox-background-sync: 7.4.1 + workbox-core: 7.4.1 + workbox-routing: 7.4.1 + workbox-strategies: 7.4.1 - workbox-navigation-preload@7.3.0: + workbox-navigation-preload@7.4.1: dependencies: - workbox-core: 7.3.0 + workbox-core: 7.4.1 - workbox-precaching@7.3.0: + workbox-precaching@7.4.1: dependencies: - workbox-core: 7.3.0 - workbox-routing: 7.3.0 - workbox-strategies: 7.3.0 + workbox-core: 7.4.1 + workbox-routing: 7.4.1 + workbox-strategies: 7.4.1 - workbox-range-requests@7.3.0: + workbox-range-requests@7.4.1: dependencies: - workbox-core: 7.3.0 + workbox-core: 7.4.1 - workbox-recipes@7.3.0: + workbox-recipes@7.4.1: dependencies: - workbox-cacheable-response: 7.3.0 - workbox-core: 7.3.0 - workbox-expiration: 7.3.0 - workbox-precaching: 7.3.0 - workbox-routing: 7.3.0 - workbox-strategies: 7.3.0 + workbox-cacheable-response: 7.4.1 + workbox-core: 7.4.1 + workbox-expiration: 7.4.1 + workbox-precaching: 7.4.1 + workbox-routing: 7.4.1 + workbox-strategies: 7.4.1 - workbox-routing@7.3.0: + workbox-routing@7.4.1: dependencies: - workbox-core: 7.3.0 + workbox-core: 7.4.1 - workbox-strategies@7.3.0: + workbox-strategies@7.4.1: dependencies: - workbox-core: 7.3.0 + workbox-core: 7.4.1 - workbox-streams@7.3.0: + workbox-streams@7.4.1: dependencies: - workbox-core: 7.3.0 - workbox-routing: 7.3.0 + workbox-core: 7.4.1 + workbox-routing: 7.4.1 - workbox-sw@7.3.0: {} - - workbox-window@7.3.0: - dependencies: - '@types/trusted-types': 2.0.7 - workbox-core: 7.3.0 + workbox-sw@7.4.1: {} workbox-window@7.4.1: dependencies: @@ -8669,8 +9016,6 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - wrappy@1.0.2: {} - xxhash-wasm@1.1.0: {} y18n@5.0.8: {} @@ -8683,7 +9028,7 @@ snapshots: ajv: 8.20.0 ajv-draft-04: 1.0.0(ajv@8.20.0) ajv-i18n: 4.2.0(ajv@8.20.0) - prettier: 3.9.4 + prettier: 3.9.5 request-light: 0.5.8 vscode-json-languageservice: 4.1.8 vscode-languageserver: 9.0.1 diff --git a/metadata/en-US/changelogs/188.txt b/metadata/en-US/changelogs/188.txt index 3d444e4f6e64..73318b0b0d49 100644 --- a/metadata/en-US/changelogs/188.txt +++ b/metadata/en-US/changelogs/188.txt @@ -7,6 +7,7 @@ * Refactor whole state management structure ([#1157](https://github.com/LinwoodDev/Butterfly/pull/1157)) * Remove unused view options * Fix crash with android saf on folders with many files +* Fix blur resetting on color change * Upgrade to agb 9 Read more here: https://linwood.dev/butterfly/2.6.0-beta.2 \ No newline at end of file From d23e7bf86950a7157e439337992be6c9db1d7624 Mon Sep 17 00:00:00 2001 From: Nezznee Date: Wed, 8 Jul 2026 19:09:06 +0200 Subject: [PATCH 054/117] Fix Polygon using aabb when closed, Typo --- app/lib/helpers/point.dart | 4 ++-- app/lib/renderers/elements/polygon.dart | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/lib/helpers/point.dart b/app/lib/helpers/point.dart index 923dfc6b3eaf..06c8875fbbaf 100644 --- a/app/lib/helpers/point.dart +++ b/app/lib/helpers/point.dart @@ -36,8 +36,8 @@ extension PathPointHelper on PathPoint { pressure, ); - PathPoint rotate(Offset center, double angle) { - final rotated = toOffset().rotate(center, angle); + PathPoint rotate(Offset center, double radians) { + final rotated = toOffset().rotate(center, radians); return PathPoint(rotated.dx, rotated.dy, pressure); } } diff --git a/app/lib/renderers/elements/polygon.dart b/app/lib/renderers/elements/polygon.dart index e91a2f39a9a9..1d47069bf9c5 100644 --- a/app/lib/renderers/elements/polygon.dart +++ b/app/lib/renderers/elements/polygon.dart @@ -325,6 +325,9 @@ class PolygonHitCalculator extends HitCalculator { collected.add(Offset(curr.x, curr.y)); } } + if (collected.length > 1 && collected.first == collected.last) { + collected.removeLast(); + } return collected; } From 910918f6401d2d9e6383b162d5d5ea23dcfa3cce Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 12 Jul 2026 14:59:19 +0200 Subject: [PATCH 055/117] Add tests for aabb test for polygon in #1162 --- app/test/renderers/polygon_renderer_test.dart | 43 +++++++ docs/package.json | 2 +- docs/pnpm-lock.yaml | 119 ++++++++---------- metadata/en-US/changelogs/188.txt | 1 + 4 files changed, 97 insertions(+), 68 deletions(-) diff --git a/app/test/renderers/polygon_renderer_test.dart b/app/test/renderers/polygon_renderer_test.dart index 105d055fe546..d7a10a09b94b 100644 --- a/app/test/renderers/polygon_renderer_test.dart +++ b/app/test/renderers/polygon_renderer_test.dart @@ -74,6 +74,49 @@ void main() { ); }); + test('closed concave polygon does not use its bounding box for hits', () { + final points = [ + PolygonPoint(0, 0), + PolygonPoint(10, 0), + PolygonPoint(10, 2), + PolygonPoint(2, 2), + PolygonPoint(2, 8), + PolygonPoint(10, 8), + PolygonPoint(10, 10), + PolygonPoint(0, 10), + PolygonPoint(0, 0), + ]; + const property = PolygonProperty(); + final openCalculator = PolygonHitCalculator( + calculatePolygonRect(points.sublist(0, points.length - 1)), + points.sublist(0, points.length - 1), + 0, + property, + ); + final closedCalculator = PolygonHitCalculator( + calculatePolygonRect(points), + points, + 0, + property, + ); + const selection = Rect.fromLTWH(5, 4, 2, 2); + + expect( + openCalculator.hit( + selection, + hitElementMode: HitElementMode.touchAnywhere, + ), + isFalse, + ); + expect( + closedCalculator.hit( + selection, + hitElementMode: HitElementMode.touchAnywhere, + ), + isFalse, + ); + }); + test('handles unbounded spacer rectangles', () { final rightSpacerRect = const Rect.fromLTRB( 50, diff --git a/docs/package.json b/docs/package.json index 8764e8249f52..2f913fa842a8 100644 --- a/docs/package.json +++ b/docs/package.json @@ -24,7 +24,7 @@ "react-dom": "^19.2.7", "typescript": "^7.0.2" }, - "packageManager": "pnpm@11.12.0", + "packageManager": "pnpm@11.11.0", "devDependencies": { "@vite-pwa/astro": "^1.2.0", "sass": "^1.101.0", diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index d22de8b8c7da..5c2e916b082f 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -16,10 +16,10 @@ importers: version: 0.3.3 '@astrojs/react': specifier: ^6.0.1 - version: 6.0.1(@types/node@26.1.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + version: 6.0.1(@types/node@26.1.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) '@astrojs/starlight': specifier: ^0.41.3 - version: 0.41.3(@astrojs/markdown-remark@7.2.1)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@7.0.2) + version: 0.41.3(@astrojs/markdown-remark@7.2.1)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@7.0.2) '@linwooddev/style': specifier: github:LinwoodDev/style#efbdf8f05d4ef1cf6fec65c90194d30b68b3c64e&path:/packages/web version: https://codeload.github.com/LinwoodDev/style/tar.gz/efbdf8f05d4ef1cf6fec65c90194d30b68b3c64e#path:/packages/web @@ -34,7 +34,7 @@ importers: version: 19.2.3(@types/react@19.2.17) astro: specifier: ^7.0.7 - version: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + version: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) katex: specifier: ^0.17.0 version: 0.17.0 @@ -50,16 +50,16 @@ importers: devDependencies: '@vite-pwa/astro': specifier: ^1.2.0 - version: 1.2.0(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1)) + version: 1.2.0(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1)) sass: specifier: ^1.101.0 version: 1.101.0 sharp: specifier: ^0.35.3 - version: 0.35.3(@types/node@26.1.0) + version: 0.35.3(@types/node@26.1.1) vite-plugin-pwa: specifier: ^1.3.0 - version: 1.3.0(vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1) + version: 1.3.0(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1) workbox-window: specifier: ^7.4.1 version: 7.4.1 @@ -1672,9 +1672,6 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - '@types/hast@3.0.5': resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} @@ -1696,8 +1693,8 @@ packages: '@types/node@24.13.3': resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} - '@types/node@26.1.0': - resolution: {integrity: sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==} + '@types/node@26.1.1': + resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} @@ -2904,10 +2901,6 @@ packages: longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - lru-cache@11.5.1: - resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} - engines: {node: 20 || >=22} - lru-cache@11.5.2: resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} @@ -4249,7 +4242,7 @@ snapshots: '@astrojs/internal-helpers@0.10.1': dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 js-yaml: 4.3.0 picomatch: 4.0.5 @@ -4312,13 +4305,13 @@ snapshots: github-slugger: 2.0.0 satteri: 0.9.5 - '@astrojs/mdx@7.0.2(@astrojs/markdown-satteri@0.3.3)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': + '@astrojs/mdx@7.0.2(@astrojs/markdown-satteri@0.3.3)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: '@astrojs/internal-helpers': 0.10.1 '@astrojs/markdown-remark': 7.2.1 '@mdx-js/mdx': 3.1.1 acorn: 8.17.0 - astro: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + astro: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) es-module-lexer: 2.3.0 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 @@ -4338,17 +4331,17 @@ snapshots: dependencies: prismjs: 1.30.0 - '@astrojs/react@6.0.1(@types/node@26.1.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)': + '@astrojs/react@6.0.1(@types/node@26.1.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)': dependencies: '@astrojs/internal-helpers': 0.10.1 '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@vitejs/plugin-react': 5.2.0(vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + '@vitejs/plugin-react': 5.2.0(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) devalue: 5.8.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) ultrahtml: 1.7.0 - vite: 8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - '@vitejs/devtools' @@ -4370,17 +4363,17 @@ snapshots: stream-replace-string: 2.0.0 zod: 4.4.3 - '@astrojs/starlight@0.41.3(@astrojs/markdown-remark@7.2.1)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@7.0.2)': + '@astrojs/starlight@0.41.3(@astrojs/markdown-remark@7.2.1)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@7.0.2)': dependencies: '@astrojs/markdown-satteri': 0.3.3 - '@astrojs/mdx': 7.0.2(@astrojs/markdown-satteri@0.3.3)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + '@astrojs/mdx': 7.0.2(@astrojs/markdown-satteri@0.3.3)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) '@astrojs/sitemap': 3.7.3 '@pagefind/default-ui': 1.5.2 '@types/hast': 3.0.5 '@types/js-yaml': 4.0.9 '@types/mdast': 4.0.4 - astro: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - astro-expressive-code: 0.44.0(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + astro: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + astro-expressive-code: 0.44.0(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) bcp-47: 2.1.1 hast-util-from-html: 2.0.3 hast-util-select: 6.0.4 @@ -5814,10 +5807,6 @@ snapshots: '@types/estree@1.0.9': {} - '@types/hast@3.0.4': - dependencies: - '@types/unist': 3.0.3 - '@types/hast@3.0.5': dependencies: '@types/unist': 3.0.3 @@ -5840,10 +5829,9 @@ snapshots: dependencies: undici-types: 7.18.2 - '@types/node@26.1.0': + '@types/node@26.1.1': dependencies: undici-types: 8.3.0 - optional: true '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: @@ -5857,7 +5845,7 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 24.13.3 + '@types/node': 26.1.1 '@types/trusted-types@2.0.7': {} @@ -5927,12 +5915,12 @@ snapshots: '@ungap/structured-clone@1.3.2': {} - '@vite-pwa/astro@1.2.0(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1))': + '@vite-pwa/astro@1.2.0(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1))': dependencies: - astro: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - vite-plugin-pwa: 1.3.0(vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1) + astro: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite-plugin-pwa: 1.3.0(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1) - '@vitejs/plugin-react@5.2.0(vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': + '@vitejs/plugin-react@5.2.0(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) @@ -5940,7 +5928,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -6055,13 +6043,13 @@ snapshots: astring@1.9.0: {} - astro-expressive-code@0.44.0(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): + astro-expressive-code@0.44.0(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): dependencies: - astro: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + astro: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) rehype-expressive-code: 0.44.0 url-extras: 0.1.0 - astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.0)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): + astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): dependencies: '@astrojs/compiler-rs': 0.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2) '@astrojs/internal-helpers': 0.10.1 @@ -6111,14 +6099,14 @@ snapshots: ultrahtml: 1.7.0 unifont: 0.7.4 unstorage: 1.17.5 - vite: 8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - vitefu: 1.1.3(vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vitefu: 1.1.3(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) xxhash-wasm: 1.1.0 yargs-parser: 22.0.0 zod: 4.4.3 optionalDependencies: '@astrojs/markdown-remark': 7.2.1 - sharp: 0.35.3(@types/node@26.1.0) + sharp: 0.35.3(@types/node@26.1.1) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -6807,7 +6795,7 @@ snapshots: hast-util-from-html@2.0.3: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 devlop: 1.1.0 hast-util-from-parse5: 8.0.3 parse5: 7.3.0 @@ -6816,7 +6804,7 @@ snapshots: hast-util-from-parse5@8.0.3: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 devlop: 1.1.0 hastscript: 9.0.1 @@ -6835,7 +6823,7 @@ snapshots: hast-util-is-element@3.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-minify-whitespace@1.0.1: dependencies: @@ -6859,7 +6847,7 @@ snapshots: hast-util-raw@9.1.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 '@ungap/structured-clone': 1.3.2 hast-util-from-parse5: 8.0.3 @@ -6914,7 +6902,7 @@ snapshots: hast-util-to-html@9.0.5: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 ccount: 2.0.1 comma-separated-tokens: 2.0.3 @@ -6948,7 +6936,7 @@ snapshots: hast-util-to-parse5@8.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 comma-separated-tokens: 2.0.3 devlop: 1.1.0 property-information: 7.2.0 @@ -6962,7 +6950,7 @@ snapshots: hast-util-to-text@4.0.2: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/unist': 3.0.3 hast-util-is-element: 3.0.0 unist-util-find-after: 5.0.0 @@ -7245,8 +7233,6 @@ snapshots: longest-streak@3.1.0: {} - lru-cache@11.5.1: {} - lru-cache@11.5.2: {} lru-cache@5.1.1: @@ -7426,7 +7412,7 @@ snapshots: mdast-util-to-hast@13.2.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 '@ungap/structured-clone': 1.3.2 devlop: 1.1.0 @@ -7862,7 +7848,7 @@ snapshots: path-scurry@2.0.2: dependencies: - lru-cache: 11.5.1 + lru-cache: 11.5.2 minipass: 7.1.3 piccolore@0.1.3: {} @@ -8017,7 +8003,7 @@ snapshots: rehype-raw@7.0.0: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-raw: 9.1.0 vfile: 6.0.3 @@ -8031,7 +8017,7 @@ snapshots: rehype-stringify@10.0.1: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 unified: 11.0.5 @@ -8080,7 +8066,7 @@ snapshots: remark-rehype@11.1.2: dependencies: - '@types/hast': 3.0.4 + '@types/hast': 3.0.5 '@types/mdast': 4.0.4 mdast-util-to-hast: 13.2.1 unified: 11.0.5 @@ -8269,7 +8255,7 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.2 - sharp@0.35.3(@types/node@26.1.0): + sharp@0.35.3(@types/node@26.1.1): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 @@ -8300,7 +8286,7 @@ snapshots: '@img/sharp-win32-arm64': 0.35.3 '@img/sharp-win32-ia32': 0.35.3 '@img/sharp-win32-x64': 0.35.3 - '@types/node': 26.1.0 + '@types/node': 26.1.1 shebang-command@2.0.0: dependencies: @@ -8586,8 +8572,7 @@ snapshots: undici-types@7.18.2: {} - undici-types@8.3.0: - optional: true + undici-types@8.3.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -8706,18 +8691,18 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-pwa@1.3.0(vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1): + vite-plugin-pwa@1.3.0(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1): dependencies: debug: 4.4.3 pretty-bytes: 6.1.1 tinyglobby: 0.2.17 - vite: 8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) workbox-build: 7.4.1(@types/babel__core@7.20.5) workbox-window: 7.4.1 transitivePeerDependencies: - supports-color - vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): + vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.5 @@ -8725,16 +8710,16 @@ snapshots: rolldown: 1.1.5 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.1.0 + '@types/node': 26.1.1 esbuild: 0.28.1 fsevents: 2.3.3 sass: 1.101.0 terser: 5.48.0 yaml: 2.9.0 - vitefu@1.1.3(vite@8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): + vitefu@1.1.3(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): optionalDependencies: - vite: 8.1.4(@types/node@26.1.0)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) volar-service-css@0.0.71(@volar/language-service@2.4.28): dependencies: diff --git a/metadata/en-US/changelogs/188.txt b/metadata/en-US/changelogs/188.txt index 73318b0b0d49..961bb5dc7893 100644 --- a/metadata/en-US/changelogs/188.txt +++ b/metadata/en-US/changelogs/188.txt @@ -8,6 +8,7 @@ * Remove unused view options * Fix crash with android saf on folders with many files * Fix blur resetting on color change +* Fix polygon collision aabb tests if closed ([#1162](https://github.com/LinwoodDev/Butterfly/pull/1162)) * Upgrade to agb 9 Read more here: https://linwood.dev/butterfly/2.6.0-beta.2 \ No newline at end of file From bcf4d1c47f52e5d9c73cc125b4920b849cb9e173 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 12 Jul 2026 17:24:34 +0200 Subject: [PATCH 056/117] Improve syncing of document states --- app/lib/dialogs/sync.dart | 7 +++++ app/lib/l10n/app_en.arb | 1 + app/lib/repositories/document_state.dart | 33 ++++++++++++++---------- app/lib/services/sync.dart | 10 +++++-- app/test/cubits/editor_session_test.dart | 22 ++++++++++++++++ 5 files changed, 58 insertions(+), 15 deletions(-) diff --git a/app/lib/dialogs/sync.dart b/app/lib/dialogs/sync.dart index 8c60d9930995..68b58acd610f 100644 --- a/app/lib/dialogs/sync.dart +++ b/app/lib/dialogs/sync.dart @@ -471,6 +471,9 @@ class _ProgressTile extends StatelessWidget { String _getFileSystemTypeLabel(BuildContext context) { return switch (type) { SyncFileSystemType.documents => AppLocalizations.of(context).document, + SyncFileSystemType.documentStates => AppLocalizations.of( + context, + ).documentStates, SyncFileSystemType.templates => AppLocalizations.of(context).templates, SyncFileSystemType.packs => AppLocalizations.of(context).packs, }; @@ -542,6 +545,7 @@ class _ProgressTile extends StatelessWidget { PhosphorIconData _getIcon() => switch (type) { SyncFileSystemType.documents => PhosphorIconsLight.file, + SyncFileSystemType.documentStates => PhosphorIconsLight.database, SyncFileSystemType.templates => PhosphorIconsLight.fileDashed, SyncFileSystemType.packs => PhosphorIconsLight.package, }; @@ -578,6 +582,9 @@ class _SyncFileCard extends StatelessWidget { String _getFileSystemTypeLabel(BuildContext context) { return switch (fileSystemType) { SyncFileSystemType.documents => AppLocalizations.of(context).document, + SyncFileSystemType.documentStates => AppLocalizations.of( + context, + ).documentStates, SyncFileSystemType.templates => AppLocalizations.of(context).templates, SyncFileSystemType.packs => AppLocalizations.of(context).packs, }; diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 11bbaa2adb79..764f06bd6b2e 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -361,6 +361,7 @@ "description": "Insert action" }, "document": "Document", + "documentStates": "Document states", "camera": "Camera", "@camera": { "description": "Camera tool" diff --git a/app/lib/repositories/document_state.dart b/app/lib/repositories/document_state.dart index ecdb942095c5..46c97d9d16b4 100644 --- a/app/lib/repositories/document_state.dart +++ b/app/lib/repositories/document_state.dart @@ -3,7 +3,8 @@ import 'dart:async'; import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/models/persisted_document_state.dart'; -import 'package:flutter/foundation.dart'; +import 'package:butterfly/services/logger.dart'; +import 'package:lw_file_system/lw_file_system.dart'; import 'package:synchronized/synchronized.dart'; class DocumentStateRepository { @@ -26,16 +27,22 @@ class DocumentStateRepository { }) => _lock.synchronized(() async { final settings = _settings; if (!settings.enabled) return null; - await fileSystem.initialize(); - if (pathKey != null) { - final byPath = await _getFileOrNull(pathKey); - if (byPath != null) return _applySettings(byPath, settings); - } - if (allowContentHash && contentHash != null) { - final byContent = await _getFileOrNull( - documentStateContentKey(contentHash), - ); - if (byContent != null) return _applySettings(byContent, settings); + try { + await fileSystem.initialize(); + if (pathKey != null) { + final byPath = await _getFileOrNull(pathKey); + if (byPath != null) return _applySettings(byPath, settings); + } + if (allowContentHash && contentHash != null) { + final byContent = await _getFileOrNull( + documentStateContentKey(contentHash), + ); + if (byContent != null) return _applySettings(byContent, settings); + } + } on NetworkException catch (e, stackTrace) { + // Document state is optional. A cached remote document must still open + // when its separate state record is unavailable offline. + talker.warning('Failed to load document state', e, stackTrace); } return null; }); @@ -215,8 +222,8 @@ class DocumentStateRepository { Future _getFileOrNull(String key) async { try { return await fileSystem.getFile(key); - } on FormatException catch (e) { - debugPrint('Failed to parse document state at $key: $e'); + } on FormatException catch (e, stackTrace) { + talker.warning('Failed to parse document state at $key', e, stackTrace); return null; } } diff --git a/app/lib/services/sync.dart b/app/lib/services/sync.dart index 7245af17e689..a4dee7c43df9 100644 --- a/app/lib/services/sync.dart +++ b/app/lib/services/sync.dart @@ -14,13 +14,15 @@ import '../cubits/settings.dart'; /// /// Each type corresponds to a different data category in the application: /// - [documents]: User-created documents and notes +/// - [documentStates]: Persisted editor state cached for offline use /// - [templates]: Reusable document templates /// - [packs]: Asset packs and resources -enum SyncFileSystemType { documents, templates, packs } +enum SyncFileSystemType { documents, documentStates, templates, packs } extension SyncFileSystemTypeHelper on SyncFileSystemType { String get cacheVariant => switch (this) { SyncFileSystemType.documents => 'documents', + SyncFileSystemType.documentStates => 'documentstates', SyncFileSystemType.templates => 'templates', SyncFileSystemType.packs => 'packs', }; @@ -168,12 +170,14 @@ class RemoteSync { final DocumentFileSystem documentSystem; final TemplateFileSystem templateSystem; final PackFileSystem packSystem; + final DocumentStateFileSystem documentStateSystem; RemoteSync(this.fileSystem, this.storage) : _stateSubject = BehaviorSubject.seeded(RemoteSyncState(storage: storage)), documentSystem = fileSystem.buildDocumentSystem(storage), templateSystem = fileSystem.buildTemplateSystem(storage), - packSystem = fileSystem.buildPackSystem(storage) { + packSystem = fileSystem.buildPackSystem(storage), + documentStateSystem = fileSystem.buildDocumentStateSystem(storage) { _initFileSystems(); unawaited(refreshFiles()); } @@ -181,6 +185,7 @@ class RemoteSync { void _initFileSystems() { // Subscribe to progress streams from remote file systems _subscribeToRemoteSystem(SyncFileSystemType.documents); + _subscribeToRemoteSystem(SyncFileSystemType.documentStates); _subscribeToRemoteSystem(SyncFileSystemType.templates); _subscribeToRemoteSystem(SyncFileSystemType.packs); } @@ -228,6 +233,7 @@ class RemoteSync { RemoteFileSystem? _getRemoteSystem(SyncFileSystemType type) { final system = switch (type) { SyncFileSystemType.documents => documentSystem.remoteSystem, + SyncFileSystemType.documentStates => documentStateSystem.remoteSystem, SyncFileSystemType.templates => templateSystem.remoteSystem, SyncFileSystemType.packs => packSystem.remoteSystem, }; diff --git a/app/test/cubits/editor_session_test.dart b/app/test/cubits/editor_session_test.dart index 48e909a6b00c..70e688220d11 100644 --- a/app/test/cubits/editor_session_test.dart +++ b/app/test/cubits/editor_session_test.dart @@ -9,9 +9,13 @@ import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lw_file_system/lw_file_system.dart'; +import 'package:mocktail/mocktail.dart'; import '../helpers/mocks.dart'; +class _MockDocumentStateFileSystem extends Mock + implements DocumentStateFileSystem {} + void main() { group('PersistedDocumentState', () { test('round trips through typed encoding', () { @@ -100,6 +104,24 @@ void main() { expect(loaded, isNull); }); + test('ignores unavailable remote state when opening offline', () async { + final offlineFileSystem = _MockDocumentStateFileSystem(); + when(() => offlineFileSystem.initialize()).thenAnswer((_) async {}); + when(() => offlineFileSystem.getFile(any())).thenThrow( + const NetworkException('Offline', type: NetworkErrorType.connection), + ); + + final loaded = await DocumentStateRepository( + offlineFileSystem, + ).load(contentHash: 'hash-a', pathKey: 'path/a'); + + expect(loaded, isNull); + verify(() => offlineFileSystem.getFile('path/a')).called(1); + verifyNever( + () => offlineFileSystem.getFile(documentStateContentKey('hash-a')), + ); + }); + test('does not load or save when persistence is disabled', () async { const state = PersistedDocumentState(pageName: 'Page 1'); await fileSystem.initialize(); From e873ae46a594adf8653ebb6e1c1cbfbad64d3be6 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Fri, 10 Jul 2026 21:52:02 +0200 Subject: [PATCH 057/117] Improve viewport performance on large documents --- app/lib/cubits/editor_renderer.dart | 94 ++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 9 deletions(-) diff --git a/app/lib/cubits/editor_renderer.dart b/app/lib/cubits/editor_renderer.dart index 5592c886b49c..80ad2dd5c286 100644 --- a/app/lib/cubits/editor_renderer.dart +++ b/app/lib/cubits/editor_renderer.dart @@ -16,6 +16,65 @@ sealed class RendererRuntimeState with _$RendererRuntimeState { }; } +class _RendererSpatialIndex { + _RendererSpatialIndex(List> renderers) { + for (var index = 0; index < renderers.length; index++) { + final bounds = renderers[index].expandedRect; + if (bounds == null) { + _unbounded.add(index); + continue; + } + final left = (bounds.left / _cellSize).floor(); + final top = (bounds.top / _cellSize).floor(); + final right = (bounds.right / _cellSize).floor(); + final bottom = (bounds.bottom / _cellSize).floor(); + final cellCount = (right - left + 1) * (bottom - top + 1); + if (cellCount > _maxCellsPerRenderer) { + _large.add(index); + continue; + } + for (var x = left; x <= right; x++) { + for (var y = top; y <= bottom; y++) { + (_cells[(x, y)] ??= []).add(index); + } + } + } + } + + static const double _cellSize = 1024; + static const int _maxCellsPerRenderer = 64; + static const int _maxQueryCells = 4096; + final Map<(int, int), List> _cells = {}; + final List _large = []; + final List _unbounded = []; + + List> query( + List> renderers, + Rect rect, + ) { + final indices = {..._large, ..._unbounded}; + final left = (rect.left / _cellSize).floor(); + final top = (rect.top / _cellSize).floor(); + final right = (rect.right / _cellSize).floor(); + final bottom = (rect.bottom / _cellSize).floor(); + final queryCellCount = (right - left + 1) * (bottom - top + 1); + if (queryCellCount > _maxQueryCells) { + return renderers.where((renderer) => renderer.isVisible(rect)).toList(); + } + for (var x = left; x <= right; x++) { + for (var y = top; y <= bottom; y++) { + final cell = _cells[(x, y)]; + if (cell != null) indices.addAll(cell); + } + } + final ordered = indices.toList()..sort(); + return ordered + .where((index) => renderers[index].isVisible(rect)) + .map((index) => renderers[index]) + .toList(growable: false); + } +} + class RendererCubit extends Cubit { RendererCubit( this.settingsCubit, [ @@ -32,6 +91,8 @@ class RendererCubit extends Cubit { EditorController? _controller; StreamSubscription? _transformSubscription; Timer? _transformDebounceTimer; + _RendererSpatialIndex? _spatialIndex; + List>? _indexedRenderers; void bindController(EditorController controller) { _controller = controller; @@ -93,6 +154,21 @@ class RendererCubit extends Cubit { List>.from(state.cameraViewport.bakedElements) ..addAll(state.cameraViewport.unbakedElements); + void _invalidateSpatialIndex() { + _spatialIndex = null; + _indexedRenderers = null; + } + + List> visibleRenderers(Rect rect) { + var all = _indexedRenderers; + if (all == null) { + all = renderers; + _indexedRenderers = all; + _spatialIndex = _RendererSpatialIndex(all); + } + return _spatialIndex!.query(all, rect); + } + Renderer? getRenderer(PadElement element) => renderers.firstWhereOrNull((renderer) => renderer.element == element); @@ -219,17 +295,14 @@ class RendererCubit extends Cubit { ) async { if (controller.isClosed) return; final unbaked = state.cameraViewport.unbakedElements; - final baked = state.cameraViewport.bakedElements; final rect = getViewportRect(controller.transformCubit); final currentVisible = state.cameraViewport.visibleElements; final currentVisibleUnbaked = state.cameraViewport.visibleUnbakedElements; - final visibleUnbaked = unbaked.where((e) => e.isVisible(rect)).toList(); - final visible = >[ - ...baked.where((e) => e.isVisible(rect)), - ...visibleUnbaked, - ]; + final visible = visibleRenderers(rect); + final unbakedSet = unbaked.toSet(); + final visibleUnbaked = visible.where(unbakedSet.contains).toList(); if (sameRendererList(visible, currentVisible) && sameRendererList(visibleUnbaked, currentVisibleUnbaked)) { @@ -417,9 +490,7 @@ class RendererCubit extends Cubit { ); if (reset) { - visibleElements = renderers - .where((renderer) => renderer.isVisible(rect)) - .toList(); + visibleElements = rendererCubit.visibleRenderers(rect); } else { final oldVisibleSet = oldVisible.toSet(); visibleElements = List.from(oldVisible) @@ -781,6 +852,7 @@ class RendererCubit extends Cubit { }) async { final rendererCubit = this; final transformCubit = controller.transformCubit; + if (unbakedElements != null) rendererCubit._invalidateSpatialIndex(); final elementsToCheck = unbakedElements ?? rendererCubit.renderers; final oldViewport = rendererCubit.state.cameraViewport; final newViewport = oldViewport.unbake( @@ -803,6 +875,7 @@ class RendererCubit extends Cubit { List>? backgrounds, }) async { final rendererCubit = this; + rendererCubit._invalidateSpatialIndex(); final transformCubit = controller.transformCubit; final visibleElements = unbakedElements .where( @@ -825,6 +898,7 @@ class RendererCubit extends Cubit { bool reset = false, }) async { final rendererCubit = this; + rendererCubit._invalidateSpatialIndex(); final transformCubit = controller.transformCubit; if (docState is! DocumentLoaded) return; final document = docState.data; @@ -932,6 +1006,7 @@ class RendererCubit extends Cubit { List>? visibleElements, ]) async { final rendererCubit = this; + rendererCubit._invalidateSpatialIndex(); final transformCubit = controller.transformCubit; final rect = rendererCubit.getViewportRect(transformCubit); visibleElements ??= unbakedElements @@ -1044,6 +1119,7 @@ class RendererCubit extends Cubit { delayedBakeRunner.cancel(); await delayedBakeRunner.disposeAndWait(); initializedElements.clear(); + _invalidateSpatialIndex(); state.cameraViewport.disposeImages(); for (final renderer in renderers) { renderer.dispose(); From 5808263dde0de91f05286dc9333197615d081c34 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Fri, 10 Jul 2026 21:59:31 +0200 Subject: [PATCH 058/117] Fix malformed pen strokes and unreliable stroke erasing on large documents --- CHANGELOG.md | 8 ++--- app/lib/bloc/document_bloc.dart | 8 +++++ app/lib/cubits/editor_tool.dart | 17 +++++++++- app/lib/handlers/eraser.dart | 38 +++++++++++++++++++++- app/lib/handlers/pen.dart | 2 +- app/test/handlers/eraser_handler_test.dart | 23 +++++++++++++ 6 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 app/test/handlers/eraser_handler_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 98bd4a6cf8df..236fe759c96e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ # Changelog - - -## 2.6.0-beta.1 (2026-07-06) - + + +## 2.6.0-beta.1 (2026-07-06) + * Add pages selector with range input ([#1151](https://github.com/LinwoodDev/Butterfly/issues/1151)) * Add internal page numbers to the pages navigator ([#1143](https://github.com/LinwoodDev/Butterfly/issues/1143)) * Add cross-page area selection and deletion ([#1143](https://github.com/LinwoodDev/Butterfly/issues/1143)) diff --git a/app/lib/bloc/document_bloc.dart b/app/lib/bloc/document_bloc.dart index 2cf5e48fd69d..c6f42761bafd 100644 --- a/app/lib/bloc/document_bloc.dart +++ b/app/lib/bloc/document_bloc.dart @@ -1746,6 +1746,14 @@ class DocumentBloc extends ReplayBloc { return cubit.toolCubit.refreshForegrounds(cubit, current); } + /// Coalesces high-frequency foreground updates to at most once per frame. + Future delayedRefreshForegrounds() async { + final current = state; + final cubit = _editorController; + if (current is! DocumentLoadSuccess || cubit == null) return; + return cubit.toolCubit.delayedRefreshForegrounds(cubit, current); + } + /// Ultra-lightweight update for cursor changes only. void updateCursor(MouseCursor cursor) { _editorController?.toolCubit.setCursor(cursor); diff --git a/app/lib/cubits/editor_tool.dart b/app/lib/cubits/editor_tool.dart index 9e644ef9a580..34bfd7b15335 100644 --- a/app/lib/cubits/editor_tool.dart +++ b/app/lib/cubits/editor_tool.dart @@ -38,6 +38,9 @@ class ToolCubit extends Cubit { : super(initial ?? ToolRuntimeState(handler: HandHandler())); final foregroundRefreshRunner = CoalescedAsyncRunner(delay: Duration.zero); + final delayedForegroundRefreshRunner = CoalescedAsyncRunner( + delay: const Duration(milliseconds: 16), + ); EditorController? _controller; Timer? _networkingDebounceTimer; @@ -929,7 +932,17 @@ class ToolCubit extends Cubit { Future refreshForegrounds( EditorController controller, DocumentLoaded blocState, - ) => foregroundRefreshRunner.schedule( + ) { + delayedForegroundRefreshRunner.cancel(); + return foregroundRefreshRunner.schedule( + () => _refreshForegrounds(controller, blocState), + ); + } + + Future delayedRefreshForegrounds( + EditorController controller, + DocumentLoaded blocState, + ) => delayedForegroundRefreshRunner.schedule( () => _refreshForegrounds(controller, blocState), ); @@ -1072,6 +1085,8 @@ class ToolCubit extends Cubit { } foregroundRefreshRunner.cancel(); await foregroundRefreshRunner.disposeAndWait(); + delayedForegroundRefreshRunner.cancel(); + await delayedForegroundRefreshRunner.disposeAndWait(); _networkingDebounceTimer?.cancel(); _networkingDebounceTimer = null; _controller = null; diff --git a/app/lib/handlers/eraser.dart b/app/lib/handlers/eraser.dart index 003c3009e8c1..a16c1f75f207 100644 --- a/app/lib/handlers/eraser.dart +++ b/app/lib/handlers/eraser.dart @@ -1,5 +1,37 @@ part of 'handler.dart'; +@visibleForTesting +List samplePenPointsForEraser( + List points, + double maxSpacing, +) { + if (points.length < 2 || maxSpacing <= 0) return points; + final sampled = [points.first]; + for (var i = 1; i < points.length; i++) { + final start = points[i - 1]; + final end = points[i]; + final dx = end.x - start.x; + final dy = end.y - start.y; + final distance = sqrt(dx * dx + dy * dy); + final steps = min(4096, max(1, (distance / maxSpacing).ceil())); + for (var step = 1; step <= steps; step++) { + if (step == steps) { + sampled.add(end); + continue; + } + final t = step / steps; + sampled.add( + PathPoint( + start.x + dx * t, + start.y + dy * t, + start.pressure + (end.pressure - start.pressure) * t, + ), + ); + } + } + return sampled; +} + class EraserHandler extends Handler { bool _currentlyErasing = false; bool _submittedPathErase = false; @@ -202,7 +234,11 @@ class EraserHandler extends Handler { List> paths = [[]]; bool changed = false; - for (final point in element.points) { + final sampledPoints = samplePenPointsForEraser( + element.points, + max(precisionErrorTolerance, sqrt(limitSquared) / 2), + ); + for (final point in sampledPoints) { final dx = point.x - globalPos.dx; final dy = point.y - globalPos.dy; if (dx * dx + dy * dy >= limitSquared) { diff --git a/app/lib/handlers/pen.dart b/app/lib/handlers/pen.dart index 6695af223bba..a56562396dae 100644 --- a/app/lib/handlers/pen.dart +++ b/app/lib/handlers/pen.dart @@ -204,7 +204,7 @@ class PenHandler extends Handler with ColoredHandler { points: points, ); } - if (refresh) bloc.refreshForegrounds(); + if (refresh) unawaited(bloc.delayedRefreshForegrounds()); } // This function is called when the pointer is pressed down. diff --git a/app/test/handlers/eraser_handler_test.dart b/app/test/handlers/eraser_handler_test.dart new file mode 100644 index 000000000000..0c36a4cf0641 --- /dev/null +++ b/app/test/handlers/eraser_handler_test.dart @@ -0,0 +1,23 @@ +import 'package:butterfly/handlers/handler.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('samples sparse pen segments for stroke erasing', () { + const points = [PathPoint(0, 0, 0.5), PathPoint(100, 0, 1)]; + + final sampled = samplePenPointsForEraser(points, 10); + + expect(sampled, hasLength(11)); + expect(sampled.first, points.first); + expect(sampled.last, points.last); + expect(sampled[5].x, 50); + expect(sampled[5].pressure, 0.75); + }); + + test('does not resample already isolated pen points', () { + const points = [PathPoint(12, 34, 0.8)]; + + expect(samplePenPointsForEraser(points, 10), same(points)); + }); +} From b7d8a6158147feeb01fb2ca3c3e7390d5f6379cf Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Fri, 10 Jul 2026 22:39:05 +0200 Subject: [PATCH 059/117] Improve pen preview performance --- app/lib/cubits/editor_tool.dart | 2 ++ app/lib/handlers/pen.dart | 22 +++++++++++++++++++++- app/test/handlers/pen_handler_test.dart | 22 ++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 app/test/handlers/pen_handler_test.dart diff --git a/app/lib/cubits/editor_tool.dart b/app/lib/cubits/editor_tool.dart index 34bfd7b15335..ead56fac9720 100644 --- a/app/lib/cubits/editor_tool.dart +++ b/app/lib/cubits/editor_tool.dart @@ -790,6 +790,8 @@ class ToolCubit extends Cubit { DocumentLoaded blocState, { bool allowBake = true, }) async { + // A full refresh supersedes any frame-delayed drawing preview. + delayedForegroundRefreshRunner.cancel(); talker.verbose('Refreshing tools'); final document = blocState.data; final page = blocState.page; diff --git a/app/lib/handlers/pen.dart b/app/lib/handlers/pen.dart index a56562396dae..f5999449f4db 100644 --- a/app/lib/handlers/pen.dart +++ b/app/lib/handlers/pen.dart @@ -1,5 +1,20 @@ part of 'handler.dart'; +@visibleForTesting +List limitPenPreviewPoints( + List points, [ + int maxPoints = 512, +]) { + if (points.length <= maxPoints || maxPoints < 2) return points; + final result = [points.first]; + final step = (points.length - 1) / (maxPoints - 1); + for (var i = 1; i < maxPoints - 1; i++) { + result.add(points[(i * step).round()]); + } + result.add(points.last); + return result; +} + // This class represents the handler for the PenTool. class PenHandler extends Handler with ColoredHandler { bool _hideCursorWhileDrawing = false; @@ -32,7 +47,12 @@ class PenHandler extends Handler with ColoredHandler { ]) => [...elements.values, ..._submittedElements] .map( (e) => e.points.length > 1 - ? PenRenderer(e.copyWith(id: createUniqueId())) + ? PenRenderer( + e.copyWith( + id: createUniqueId(), + points: limitPenPreviewPoints(e.points), + ), + ) : null, ) .whereType() diff --git a/app/test/handlers/pen_handler_test.dart b/app/test/handlers/pen_handler_test.dart new file mode 100644 index 000000000000..00306b04c0e0 --- /dev/null +++ b/app/test/handlers/pen_handler_test.dart @@ -0,0 +1,22 @@ +import 'package:butterfly/handlers/handler.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('bounds preview work while preserving stroke endpoints', () { + final points = List.generate(1000, (i) => PathPoint(i.toDouble(), 0)); + + final preview = limitPenPreviewPoints(points, 100); + + expect(preview, hasLength(100)); + expect(preview.first, same(points.first)); + expect(preview.last, same(points.last)); + expect(points, hasLength(1000)); + }); + + test('keeps short previews unchanged', () { + final points = [const PathPoint(0, 0), const PathPoint(1, 1)]; + + expect(limitPenPreviewPoints(points), same(points)); + }); +} From 22aeb0b55b3df8a7fdaac7864b858151131d510b Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sat, 11 Jul 2026 10:22:07 +0200 Subject: [PATCH 060/117] Fix layout shifting --- app/lib/cubits/editor_renderer.dart | 6 ++-- app/lib/models/viewport.dart | 5 +++ app/lib/models/viewport.freezed.dart | 47 +++++++++++++++------------ app/test/bloc/document_bloc_test.dart | 37 +++++++++++++++++++++ 4 files changed, 72 insertions(+), 23 deletions(-) diff --git a/app/lib/cubits/editor_renderer.dart b/app/lib/cubits/editor_renderer.dart index 80ad2dd5c286..312750dc1079 100644 --- a/app/lib/cubits/editor_renderer.dart +++ b/app/lib/cubits/editor_renderer.dart @@ -427,12 +427,13 @@ class RendererCubit extends Cubit { final startTransform = transformCubit.state; final startViewport = cameraViewport; final resolution = settingsCubit.state.renderResolution; - var size = viewportSize ?? cameraViewport.toSize(); + final measuredViewportSize = viewportSize ?? cameraViewport.viewportSize; + var size = measuredViewportSize ?? cameraViewport.toSize(); final ratio = pixelRatio ?? cameraViewport.pixelRatio; if (size.height <= 0 || size.width <= 0) { return; } - if (viewportSize == null) { + if (measuredViewportSize == null) { size /= resolution.multiplier; } var transform = transformCubit.state; @@ -682,6 +683,7 @@ class RendererCubit extends Cubit { final newViewport = cameraViewport.bake( height: size.height, width: size.width, + viewportSize: measuredViewportSize, pixelRatio: ratio, resolution: resolution, scale: transform.size, diff --git a/app/lib/models/viewport.dart b/app/lib/models/viewport.dart index 628949a85e54..ba0b2400d3ed 100644 --- a/app/lib/models/viewport.dart +++ b/app/lib/models/viewport.dart @@ -25,6 +25,7 @@ sealed class CameraViewport with _$CameraViewport { @Default([]) List> visibleUnbakedElements, double? width, double? height, + ui.Size? viewportSize, @Default(1) double pixelRatio, @Default(1) double scale, @Default(0) double x, @@ -44,6 +45,7 @@ sealed class CameraViewport with _$CameraViewport { ui.Image? aboveLayerImage, required double? width, required double? height, + ui.Size? viewportSize, required double pixelRatio, @Default([]) List> bakedElements, @Default([]) List> unbakedElements, @@ -194,6 +196,7 @@ sealed class CameraViewport with _$CameraViewport { rendererStates: rendererStates ?? this.rendererStates, width: width, height: height, + viewportSize: viewportSize, pixelRatio: pixelRatio, scale: scale, x: x, @@ -207,6 +210,7 @@ sealed class CameraViewport with _$CameraViewport { required ui.Image image, required double width, required double height, + ui.Size? viewportSize, required double pixelRatio, ui.Image? belowLayerImage, ui.Image? aboveLayerImage, @@ -225,6 +229,7 @@ sealed class CameraViewport with _$CameraViewport { image: image, width: width, height: height, + viewportSize: viewportSize ?? this.viewportSize, scale: scale, pixelRatio: pixelRatio, bakedElements: bakedElements, diff --git a/app/lib/models/viewport.freezed.dart b/app/lib/models/viewport.freezed.dart index 17ed27dea82d..41e7835de6ec 100644 --- a/app/lib/models/viewport.freezed.dart +++ b/app/lib/models/viewport.freezed.dart @@ -14,7 +14,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$CameraViewport { - List> get backgrounds; List> get bakedElements; List> get unbakedElements; List> get visibleElements; List> get visibleUnbakedElements; double? get width; double? get height; double get pixelRatio; double get scale; double get x; double get y; RenderResolution get resolution; Map get rendererStates; Set get invisibleLayers; ui.Image? get image; ui.Image? get belowLayerImage; ui.Image? get aboveLayerImage; + List> get backgrounds; List> get bakedElements; List> get unbakedElements; List> get visibleElements; List> get visibleUnbakedElements; double? get width; double? get height; ui.Size? get viewportSize; double get pixelRatio; double get scale; double get x; double get y; RenderResolution get resolution; Map get rendererStates; Set get invisibleLayers; ui.Image? get image; ui.Image? get belowLayerImage; ui.Image? get aboveLayerImage; /// Create a copy of CameraViewport /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -25,16 +25,16 @@ $CameraViewportCopyWith get copyWith => _$CameraViewportCopyWith @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is CameraViewport&&const DeepCollectionEquality().equals(other.backgrounds, backgrounds)&&const DeepCollectionEquality().equals(other.bakedElements, bakedElements)&&const DeepCollectionEquality().equals(other.unbakedElements, unbakedElements)&&const DeepCollectionEquality().equals(other.visibleElements, visibleElements)&&const DeepCollectionEquality().equals(other.visibleUnbakedElements, visibleUnbakedElements)&&(identical(other.width, width) || other.width == width)&&(identical(other.height, height) || other.height == height)&&(identical(other.pixelRatio, pixelRatio) || other.pixelRatio == pixelRatio)&&(identical(other.scale, scale) || other.scale == scale)&&(identical(other.x, x) || other.x == x)&&(identical(other.y, y) || other.y == y)&&(identical(other.resolution, resolution) || other.resolution == resolution)&&const DeepCollectionEquality().equals(other.rendererStates, rendererStates)&&const DeepCollectionEquality().equals(other.invisibleLayers, invisibleLayers)&&(identical(other.image, image) || other.image == image)&&(identical(other.belowLayerImage, belowLayerImage) || other.belowLayerImage == belowLayerImage)&&(identical(other.aboveLayerImage, aboveLayerImage) || other.aboveLayerImage == aboveLayerImage)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is CameraViewport&&const DeepCollectionEquality().equals(other.backgrounds, backgrounds)&&const DeepCollectionEquality().equals(other.bakedElements, bakedElements)&&const DeepCollectionEquality().equals(other.unbakedElements, unbakedElements)&&const DeepCollectionEquality().equals(other.visibleElements, visibleElements)&&const DeepCollectionEquality().equals(other.visibleUnbakedElements, visibleUnbakedElements)&&(identical(other.width, width) || other.width == width)&&(identical(other.height, height) || other.height == height)&&(identical(other.viewportSize, viewportSize) || other.viewportSize == viewportSize)&&(identical(other.pixelRatio, pixelRatio) || other.pixelRatio == pixelRatio)&&(identical(other.scale, scale) || other.scale == scale)&&(identical(other.x, x) || other.x == x)&&(identical(other.y, y) || other.y == y)&&(identical(other.resolution, resolution) || other.resolution == resolution)&&const DeepCollectionEquality().equals(other.rendererStates, rendererStates)&&const DeepCollectionEquality().equals(other.invisibleLayers, invisibleLayers)&&(identical(other.image, image) || other.image == image)&&(identical(other.belowLayerImage, belowLayerImage) || other.belowLayerImage == belowLayerImage)&&(identical(other.aboveLayerImage, aboveLayerImage) || other.aboveLayerImage == aboveLayerImage)); } @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(backgrounds),const DeepCollectionEquality().hash(bakedElements),const DeepCollectionEquality().hash(unbakedElements),const DeepCollectionEquality().hash(visibleElements),const DeepCollectionEquality().hash(visibleUnbakedElements),width,height,pixelRatio,scale,x,y,resolution,const DeepCollectionEquality().hash(rendererStates),const DeepCollectionEquality().hash(invisibleLayers),image,belowLayerImage,aboveLayerImage); +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(backgrounds),const DeepCollectionEquality().hash(bakedElements),const DeepCollectionEquality().hash(unbakedElements),const DeepCollectionEquality().hash(visibleElements),const DeepCollectionEquality().hash(visibleUnbakedElements),width,height,viewportSize,pixelRatio,scale,x,y,resolution,const DeepCollectionEquality().hash(rendererStates),const DeepCollectionEquality().hash(invisibleLayers),image,belowLayerImage,aboveLayerImage); @override String toString() { - return 'CameraViewport(backgrounds: $backgrounds, bakedElements: $bakedElements, unbakedElements: $unbakedElements, visibleElements: $visibleElements, visibleUnbakedElements: $visibleUnbakedElements, width: $width, height: $height, pixelRatio: $pixelRatio, scale: $scale, x: $x, y: $y, resolution: $resolution, rendererStates: $rendererStates, invisibleLayers: $invisibleLayers, image: $image, belowLayerImage: $belowLayerImage, aboveLayerImage: $aboveLayerImage)'; + return 'CameraViewport(backgrounds: $backgrounds, bakedElements: $bakedElements, unbakedElements: $unbakedElements, visibleElements: $visibleElements, visibleUnbakedElements: $visibleUnbakedElements, width: $width, height: $height, viewportSize: $viewportSize, pixelRatio: $pixelRatio, scale: $scale, x: $x, y: $y, resolution: $resolution, rendererStates: $rendererStates, invisibleLayers: $invisibleLayers, image: $image, belowLayerImage: $belowLayerImage, aboveLayerImage: $aboveLayerImage)'; } @@ -45,7 +45,7 @@ abstract mixin class $CameraViewportCopyWith<$Res> { factory $CameraViewportCopyWith(CameraViewport value, $Res Function(CameraViewport) _then) = _$CameraViewportCopyWithImpl; @useResult $Res call({ - List> backgrounds, List> bakedElements, List> unbakedElements, List> visibleElements, List> visibleUnbakedElements, double? width, double? height, double pixelRatio, double scale, double x, double y, RenderResolution resolution, Map rendererStates, Set invisibleLayers, ui.Image? image, ui.Image? belowLayerImage, ui.Image? aboveLayerImage + List> backgrounds, List> bakedElements, List> unbakedElements, List> visibleElements, List> visibleUnbakedElements, double? width, double? height, ui.Size? viewportSize, double pixelRatio, double scale, double x, double y, RenderResolution resolution, Map rendererStates, Set invisibleLayers, ui.Image? image, ui.Image? belowLayerImage, ui.Image? aboveLayerImage }); @@ -62,7 +62,7 @@ class _$CameraViewportCopyWithImpl<$Res> /// Create a copy of CameraViewport /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? backgrounds = null,Object? bakedElements = null,Object? unbakedElements = null,Object? visibleElements = null,Object? visibleUnbakedElements = null,Object? width = freezed,Object? height = freezed,Object? pixelRatio = null,Object? scale = null,Object? x = null,Object? y = null,Object? resolution = null,Object? rendererStates = null,Object? invisibleLayers = null,Object? image = freezed,Object? belowLayerImage = freezed,Object? aboveLayerImage = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? backgrounds = null,Object? bakedElements = null,Object? unbakedElements = null,Object? visibleElements = null,Object? visibleUnbakedElements = null,Object? width = freezed,Object? height = freezed,Object? viewportSize = freezed,Object? pixelRatio = null,Object? scale = null,Object? x = null,Object? y = null,Object? resolution = null,Object? rendererStates = null,Object? invisibleLayers = null,Object? image = freezed,Object? belowLayerImage = freezed,Object? aboveLayerImage = freezed,}) { return _then(_self.copyWith( backgrounds: null == backgrounds ? _self.backgrounds : backgrounds // ignore: cast_nullable_to_non_nullable as List>,bakedElements: null == bakedElements ? _self.bakedElements : bakedElements // ignore: cast_nullable_to_non_nullable @@ -71,7 +71,8 @@ as List>,visibleElements: null == visibleElements ? _self.v as List>,visibleUnbakedElements: null == visibleUnbakedElements ? _self.visibleUnbakedElements : visibleUnbakedElements // ignore: cast_nullable_to_non_nullable as List>,width: freezed == width ? _self.width : width // ignore: cast_nullable_to_non_nullable as double?,height: freezed == height ? _self.height : height // ignore: cast_nullable_to_non_nullable -as double?,pixelRatio: null == pixelRatio ? _self.pixelRatio : pixelRatio // ignore: cast_nullable_to_non_nullable +as double?,viewportSize: freezed == viewportSize ? _self.viewportSize : viewportSize // ignore: cast_nullable_to_non_nullable +as ui.Size?,pixelRatio: null == pixelRatio ? _self.pixelRatio : pixelRatio // ignore: cast_nullable_to_non_nullable as double,scale: null == scale ? _self.scale : scale // ignore: cast_nullable_to_non_nullable as double,x: null == x ? _self.x : x // ignore: cast_nullable_to_non_nullable as double,y: null == y ? _self.y : y // ignore: cast_nullable_to_non_nullable @@ -93,7 +94,7 @@ as ui.Image?, class CameraViewportUnbaked extends CameraViewport { - const CameraViewportUnbaked({final List> backgrounds = const [], final List> bakedElements = const [], final List> unbakedElements = const [], final List> visibleElements = const [], final List> visibleUnbakedElements = const [], this.width, this.height, this.pixelRatio = 1, this.scale = 1, this.x = 0, this.y = 0, this.resolution = RenderResolution.performance, final Map rendererStates = const {}, final Set invisibleLayers = const {}, this.image = null, this.belowLayerImage = null, this.aboveLayerImage = null}): _backgrounds = backgrounds,_bakedElements = bakedElements,_unbakedElements = unbakedElements,_visibleElements = visibleElements,_visibleUnbakedElements = visibleUnbakedElements,_rendererStates = rendererStates,_invisibleLayers = invisibleLayers,super._(); + const CameraViewportUnbaked({final List> backgrounds = const [], final List> bakedElements = const [], final List> unbakedElements = const [], final List> visibleElements = const [], final List> visibleUnbakedElements = const [], this.width, this.height, this.viewportSize, this.pixelRatio = 1, this.scale = 1, this.x = 0, this.y = 0, this.resolution = RenderResolution.performance, final Map rendererStates = const {}, final Set invisibleLayers = const {}, this.image = null, this.belowLayerImage = null, this.aboveLayerImage = null}): _backgrounds = backgrounds,_bakedElements = bakedElements,_unbakedElements = unbakedElements,_visibleElements = visibleElements,_visibleUnbakedElements = visibleUnbakedElements,_rendererStates = rendererStates,_invisibleLayers = invisibleLayers,super._(); final List> _backgrounds; @@ -133,6 +134,7 @@ class CameraViewportUnbaked extends CameraViewport { @override final double? width; @override final double? height; +@override final ui.Size? viewportSize; @override@JsonKey() final double pixelRatio; @override@JsonKey() final double scale; @override@JsonKey() final double x; @@ -166,16 +168,16 @@ $CameraViewportUnbakedCopyWith get copyWith => _$CameraVi @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is CameraViewportUnbaked&&const DeepCollectionEquality().equals(other._backgrounds, _backgrounds)&&const DeepCollectionEquality().equals(other._bakedElements, _bakedElements)&&const DeepCollectionEquality().equals(other._unbakedElements, _unbakedElements)&&const DeepCollectionEquality().equals(other._visibleElements, _visibleElements)&&const DeepCollectionEquality().equals(other._visibleUnbakedElements, _visibleUnbakedElements)&&(identical(other.width, width) || other.width == width)&&(identical(other.height, height) || other.height == height)&&(identical(other.pixelRatio, pixelRatio) || other.pixelRatio == pixelRatio)&&(identical(other.scale, scale) || other.scale == scale)&&(identical(other.x, x) || other.x == x)&&(identical(other.y, y) || other.y == y)&&(identical(other.resolution, resolution) || other.resolution == resolution)&&const DeepCollectionEquality().equals(other._rendererStates, _rendererStates)&&const DeepCollectionEquality().equals(other._invisibleLayers, _invisibleLayers)&&(identical(other.image, image) || other.image == image)&&(identical(other.belowLayerImage, belowLayerImage) || other.belowLayerImage == belowLayerImage)&&(identical(other.aboveLayerImage, aboveLayerImage) || other.aboveLayerImage == aboveLayerImage)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is CameraViewportUnbaked&&const DeepCollectionEquality().equals(other._backgrounds, _backgrounds)&&const DeepCollectionEquality().equals(other._bakedElements, _bakedElements)&&const DeepCollectionEquality().equals(other._unbakedElements, _unbakedElements)&&const DeepCollectionEquality().equals(other._visibleElements, _visibleElements)&&const DeepCollectionEquality().equals(other._visibleUnbakedElements, _visibleUnbakedElements)&&(identical(other.width, width) || other.width == width)&&(identical(other.height, height) || other.height == height)&&(identical(other.viewportSize, viewportSize) || other.viewportSize == viewportSize)&&(identical(other.pixelRatio, pixelRatio) || other.pixelRatio == pixelRatio)&&(identical(other.scale, scale) || other.scale == scale)&&(identical(other.x, x) || other.x == x)&&(identical(other.y, y) || other.y == y)&&(identical(other.resolution, resolution) || other.resolution == resolution)&&const DeepCollectionEquality().equals(other._rendererStates, _rendererStates)&&const DeepCollectionEquality().equals(other._invisibleLayers, _invisibleLayers)&&(identical(other.image, image) || other.image == image)&&(identical(other.belowLayerImage, belowLayerImage) || other.belowLayerImage == belowLayerImage)&&(identical(other.aboveLayerImage, aboveLayerImage) || other.aboveLayerImage == aboveLayerImage)); } @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_backgrounds),const DeepCollectionEquality().hash(_bakedElements),const DeepCollectionEquality().hash(_unbakedElements),const DeepCollectionEquality().hash(_visibleElements),const DeepCollectionEquality().hash(_visibleUnbakedElements),width,height,pixelRatio,scale,x,y,resolution,const DeepCollectionEquality().hash(_rendererStates),const DeepCollectionEquality().hash(_invisibleLayers),image,belowLayerImage,aboveLayerImage); +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_backgrounds),const DeepCollectionEquality().hash(_bakedElements),const DeepCollectionEquality().hash(_unbakedElements),const DeepCollectionEquality().hash(_visibleElements),const DeepCollectionEquality().hash(_visibleUnbakedElements),width,height,viewportSize,pixelRatio,scale,x,y,resolution,const DeepCollectionEquality().hash(_rendererStates),const DeepCollectionEquality().hash(_invisibleLayers),image,belowLayerImage,aboveLayerImage); @override String toString() { - return 'CameraViewport.unbaked(backgrounds: $backgrounds, bakedElements: $bakedElements, unbakedElements: $unbakedElements, visibleElements: $visibleElements, visibleUnbakedElements: $visibleUnbakedElements, width: $width, height: $height, pixelRatio: $pixelRatio, scale: $scale, x: $x, y: $y, resolution: $resolution, rendererStates: $rendererStates, invisibleLayers: $invisibleLayers, image: $image, belowLayerImage: $belowLayerImage, aboveLayerImage: $aboveLayerImage)'; + return 'CameraViewport.unbaked(backgrounds: $backgrounds, bakedElements: $bakedElements, unbakedElements: $unbakedElements, visibleElements: $visibleElements, visibleUnbakedElements: $visibleUnbakedElements, width: $width, height: $height, viewportSize: $viewportSize, pixelRatio: $pixelRatio, scale: $scale, x: $x, y: $y, resolution: $resolution, rendererStates: $rendererStates, invisibleLayers: $invisibleLayers, image: $image, belowLayerImage: $belowLayerImage, aboveLayerImage: $aboveLayerImage)'; } @@ -186,7 +188,7 @@ abstract mixin class $CameraViewportUnbakedCopyWith<$Res> implements $CameraView factory $CameraViewportUnbakedCopyWith(CameraViewportUnbaked value, $Res Function(CameraViewportUnbaked) _then) = _$CameraViewportUnbakedCopyWithImpl; @override @useResult $Res call({ - List> backgrounds, List> bakedElements, List> unbakedElements, List> visibleElements, List> visibleUnbakedElements, double? width, double? height, double pixelRatio, double scale, double x, double y, RenderResolution resolution, Map rendererStates, Set invisibleLayers, ui.Image? image, ui.Image? belowLayerImage, ui.Image? aboveLayerImage + List> backgrounds, List> bakedElements, List> unbakedElements, List> visibleElements, List> visibleUnbakedElements, double? width, double? height, ui.Size? viewportSize, double pixelRatio, double scale, double x, double y, RenderResolution resolution, Map rendererStates, Set invisibleLayers, ui.Image? image, ui.Image? belowLayerImage, ui.Image? aboveLayerImage }); @@ -203,7 +205,7 @@ class _$CameraViewportUnbakedCopyWithImpl<$Res> /// Create a copy of CameraViewport /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? backgrounds = null,Object? bakedElements = null,Object? unbakedElements = null,Object? visibleElements = null,Object? visibleUnbakedElements = null,Object? width = freezed,Object? height = freezed,Object? pixelRatio = null,Object? scale = null,Object? x = null,Object? y = null,Object? resolution = null,Object? rendererStates = null,Object? invisibleLayers = null,Object? image = freezed,Object? belowLayerImage = freezed,Object? aboveLayerImage = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? backgrounds = null,Object? bakedElements = null,Object? unbakedElements = null,Object? visibleElements = null,Object? visibleUnbakedElements = null,Object? width = freezed,Object? height = freezed,Object? viewportSize = freezed,Object? pixelRatio = null,Object? scale = null,Object? x = null,Object? y = null,Object? resolution = null,Object? rendererStates = null,Object? invisibleLayers = null,Object? image = freezed,Object? belowLayerImage = freezed,Object? aboveLayerImage = freezed,}) { return _then(CameraViewportUnbaked( backgrounds: null == backgrounds ? _self._backgrounds : backgrounds // ignore: cast_nullable_to_non_nullable as List>,bakedElements: null == bakedElements ? _self._bakedElements : bakedElements // ignore: cast_nullable_to_non_nullable @@ -212,7 +214,8 @@ as List>,visibleElements: null == visibleElements ? _self._ as List>,visibleUnbakedElements: null == visibleUnbakedElements ? _self._visibleUnbakedElements : visibleUnbakedElements // ignore: cast_nullable_to_non_nullable as List>,width: freezed == width ? _self.width : width // ignore: cast_nullable_to_non_nullable as double?,height: freezed == height ? _self.height : height // ignore: cast_nullable_to_non_nullable -as double?,pixelRatio: null == pixelRatio ? _self.pixelRatio : pixelRatio // ignore: cast_nullable_to_non_nullable +as double?,viewportSize: freezed == viewportSize ? _self.viewportSize : viewportSize // ignore: cast_nullable_to_non_nullable +as ui.Size?,pixelRatio: null == pixelRatio ? _self.pixelRatio : pixelRatio // ignore: cast_nullable_to_non_nullable as double,scale: null == scale ? _self.scale : scale // ignore: cast_nullable_to_non_nullable as double,x: null == x ? _self.x : x // ignore: cast_nullable_to_non_nullable as double,y: null == y ? _self.y : y // ignore: cast_nullable_to_non_nullable @@ -233,7 +236,7 @@ as ui.Image?, class CameraViewportBaked extends CameraViewport { - const CameraViewportBaked({final List> backgrounds = const [], this.image, this.belowLayerImage, this.aboveLayerImage, required this.width, required this.height, required this.pixelRatio, final List> bakedElements = const [], final List> unbakedElements = const [], required final List> visibleElements, required final List> visibleUnbakedElements, this.scale = 1, this.x = 0, required this.resolution, this.y = 0, final Map rendererStates = const {}, final Set invisibleLayers = const {}}): _backgrounds = backgrounds,_bakedElements = bakedElements,_unbakedElements = unbakedElements,_visibleElements = visibleElements,_visibleUnbakedElements = visibleUnbakedElements,_rendererStates = rendererStates,_invisibleLayers = invisibleLayers,super._(); + const CameraViewportBaked({final List> backgrounds = const [], this.image, this.belowLayerImage, this.aboveLayerImage, required this.width, required this.height, this.viewportSize, required this.pixelRatio, final List> bakedElements = const [], final List> unbakedElements = const [], required final List> visibleElements, required final List> visibleUnbakedElements, this.scale = 1, this.x = 0, required this.resolution, this.y = 0, final Map rendererStates = const {}, final Set invisibleLayers = const {}}): _backgrounds = backgrounds,_bakedElements = bakedElements,_unbakedElements = unbakedElements,_visibleElements = visibleElements,_visibleUnbakedElements = visibleUnbakedElements,_rendererStates = rendererStates,_invisibleLayers = invisibleLayers,super._(); final List> _backgrounds; @@ -248,6 +251,7 @@ class CameraViewportBaked extends CameraViewport { @override final ui.Image? aboveLayerImage; @override final double? width; @override final double? height; +@override final ui.Size? viewportSize; @override final double pixelRatio; final List> _bakedElements; @override@JsonKey() List> get bakedElements { @@ -306,16 +310,16 @@ $CameraViewportBakedCopyWith get copyWith => _$CameraViewpo @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is CameraViewportBaked&&const DeepCollectionEquality().equals(other._backgrounds, _backgrounds)&&(identical(other.image, image) || other.image == image)&&(identical(other.belowLayerImage, belowLayerImage) || other.belowLayerImage == belowLayerImage)&&(identical(other.aboveLayerImage, aboveLayerImage) || other.aboveLayerImage == aboveLayerImage)&&(identical(other.width, width) || other.width == width)&&(identical(other.height, height) || other.height == height)&&(identical(other.pixelRatio, pixelRatio) || other.pixelRatio == pixelRatio)&&const DeepCollectionEquality().equals(other._bakedElements, _bakedElements)&&const DeepCollectionEquality().equals(other._unbakedElements, _unbakedElements)&&const DeepCollectionEquality().equals(other._visibleElements, _visibleElements)&&const DeepCollectionEquality().equals(other._visibleUnbakedElements, _visibleUnbakedElements)&&(identical(other.scale, scale) || other.scale == scale)&&(identical(other.x, x) || other.x == x)&&(identical(other.resolution, resolution) || other.resolution == resolution)&&(identical(other.y, y) || other.y == y)&&const DeepCollectionEquality().equals(other._rendererStates, _rendererStates)&&const DeepCollectionEquality().equals(other._invisibleLayers, _invisibleLayers)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is CameraViewportBaked&&const DeepCollectionEquality().equals(other._backgrounds, _backgrounds)&&(identical(other.image, image) || other.image == image)&&(identical(other.belowLayerImage, belowLayerImage) || other.belowLayerImage == belowLayerImage)&&(identical(other.aboveLayerImage, aboveLayerImage) || other.aboveLayerImage == aboveLayerImage)&&(identical(other.width, width) || other.width == width)&&(identical(other.height, height) || other.height == height)&&(identical(other.viewportSize, viewportSize) || other.viewportSize == viewportSize)&&(identical(other.pixelRatio, pixelRatio) || other.pixelRatio == pixelRatio)&&const DeepCollectionEquality().equals(other._bakedElements, _bakedElements)&&const DeepCollectionEquality().equals(other._unbakedElements, _unbakedElements)&&const DeepCollectionEquality().equals(other._visibleElements, _visibleElements)&&const DeepCollectionEquality().equals(other._visibleUnbakedElements, _visibleUnbakedElements)&&(identical(other.scale, scale) || other.scale == scale)&&(identical(other.x, x) || other.x == x)&&(identical(other.resolution, resolution) || other.resolution == resolution)&&(identical(other.y, y) || other.y == y)&&const DeepCollectionEquality().equals(other._rendererStates, _rendererStates)&&const DeepCollectionEquality().equals(other._invisibleLayers, _invisibleLayers)); } @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_backgrounds),image,belowLayerImage,aboveLayerImage,width,height,pixelRatio,const DeepCollectionEquality().hash(_bakedElements),const DeepCollectionEquality().hash(_unbakedElements),const DeepCollectionEquality().hash(_visibleElements),const DeepCollectionEquality().hash(_visibleUnbakedElements),scale,x,resolution,y,const DeepCollectionEquality().hash(_rendererStates),const DeepCollectionEquality().hash(_invisibleLayers)); +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_backgrounds),image,belowLayerImage,aboveLayerImage,width,height,viewportSize,pixelRatio,const DeepCollectionEquality().hash(_bakedElements),const DeepCollectionEquality().hash(_unbakedElements),const DeepCollectionEquality().hash(_visibleElements),const DeepCollectionEquality().hash(_visibleUnbakedElements),scale,x,resolution,y,const DeepCollectionEquality().hash(_rendererStates),const DeepCollectionEquality().hash(_invisibleLayers)); @override String toString() { - return 'CameraViewport.baked(backgrounds: $backgrounds, image: $image, belowLayerImage: $belowLayerImage, aboveLayerImage: $aboveLayerImage, width: $width, height: $height, pixelRatio: $pixelRatio, bakedElements: $bakedElements, unbakedElements: $unbakedElements, visibleElements: $visibleElements, visibleUnbakedElements: $visibleUnbakedElements, scale: $scale, x: $x, resolution: $resolution, y: $y, rendererStates: $rendererStates, invisibleLayers: $invisibleLayers)'; + return 'CameraViewport.baked(backgrounds: $backgrounds, image: $image, belowLayerImage: $belowLayerImage, aboveLayerImage: $aboveLayerImage, width: $width, height: $height, viewportSize: $viewportSize, pixelRatio: $pixelRatio, bakedElements: $bakedElements, unbakedElements: $unbakedElements, visibleElements: $visibleElements, visibleUnbakedElements: $visibleUnbakedElements, scale: $scale, x: $x, resolution: $resolution, y: $y, rendererStates: $rendererStates, invisibleLayers: $invisibleLayers)'; } @@ -326,7 +330,7 @@ abstract mixin class $CameraViewportBakedCopyWith<$Res> implements $CameraViewpo factory $CameraViewportBakedCopyWith(CameraViewportBaked value, $Res Function(CameraViewportBaked) _then) = _$CameraViewportBakedCopyWithImpl; @override @useResult $Res call({ - List> backgrounds, ui.Image? image, ui.Image? belowLayerImage, ui.Image? aboveLayerImage, double? width, double? height, double pixelRatio, List> bakedElements, List> unbakedElements, List> visibleElements, List> visibleUnbakedElements, double scale, double x, RenderResolution resolution, double y, Map rendererStates, Set invisibleLayers + List> backgrounds, ui.Image? image, ui.Image? belowLayerImage, ui.Image? aboveLayerImage, double? width, double? height, ui.Size? viewportSize, double pixelRatio, List> bakedElements, List> unbakedElements, List> visibleElements, List> visibleUnbakedElements, double scale, double x, RenderResolution resolution, double y, Map rendererStates, Set invisibleLayers }); @@ -343,7 +347,7 @@ class _$CameraViewportBakedCopyWithImpl<$Res> /// Create a copy of CameraViewport /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? backgrounds = null,Object? image = freezed,Object? belowLayerImage = freezed,Object? aboveLayerImage = freezed,Object? width = freezed,Object? height = freezed,Object? pixelRatio = null,Object? bakedElements = null,Object? unbakedElements = null,Object? visibleElements = null,Object? visibleUnbakedElements = null,Object? scale = null,Object? x = null,Object? resolution = null,Object? y = null,Object? rendererStates = null,Object? invisibleLayers = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? backgrounds = null,Object? image = freezed,Object? belowLayerImage = freezed,Object? aboveLayerImage = freezed,Object? width = freezed,Object? height = freezed,Object? viewportSize = freezed,Object? pixelRatio = null,Object? bakedElements = null,Object? unbakedElements = null,Object? visibleElements = null,Object? visibleUnbakedElements = null,Object? scale = null,Object? x = null,Object? resolution = null,Object? y = null,Object? rendererStates = null,Object? invisibleLayers = null,}) { return _then(CameraViewportBaked( backgrounds: null == backgrounds ? _self._backgrounds : backgrounds // ignore: cast_nullable_to_non_nullable as List>,image: freezed == image ? _self.image : image // ignore: cast_nullable_to_non_nullable @@ -351,7 +355,8 @@ as ui.Image?,belowLayerImage: freezed == belowLayerImage ? _self.belowLayerImage as ui.Image?,aboveLayerImage: freezed == aboveLayerImage ? _self.aboveLayerImage : aboveLayerImage // ignore: cast_nullable_to_non_nullable as ui.Image?,width: freezed == width ? _self.width : width // ignore: cast_nullable_to_non_nullable as double?,height: freezed == height ? _self.height : height // ignore: cast_nullable_to_non_nullable -as double?,pixelRatio: null == pixelRatio ? _self.pixelRatio : pixelRatio // ignore: cast_nullable_to_non_nullable +as double?,viewportSize: freezed == viewportSize ? _self.viewportSize : viewportSize // ignore: cast_nullable_to_non_nullable +as ui.Size?,pixelRatio: null == pixelRatio ? _self.pixelRatio : pixelRatio // ignore: cast_nullable_to_non_nullable as double,bakedElements: null == bakedElements ? _self._bakedElements : bakedElements // ignore: cast_nullable_to_non_nullable as List>,unbakedElements: null == unbakedElements ? _self._unbakedElements : unbakedElements // ignore: cast_nullable_to_non_nullable as List>,visibleElements: null == visibleElements ? _self._visibleElements : visibleElements // ignore: cast_nullable_to_non_nullable diff --git a/app/test/bloc/document_bloc_test.dart b/app/test/bloc/document_bloc_test.dart index 9e42651bd91a..001959700f5d 100644 --- a/app/test/bloc/document_bloc_test.dart +++ b/app/test/bloc/document_bloc_test.dart @@ -1214,6 +1214,43 @@ void main() { }, ); + test('no-argument bake reuses the measured viewport size', () async { + when(() => settingsCubit.state).thenReturn( + const ButterflySettings( + autosave: false, + renderResolution: RenderResolution.normal, + ), + ); + const measuredSize = Size(401.25, 303.75); + + await editorController.rendererCubit.bake( + editorController, + bloc.state as DocumentLoadSuccess, + viewportSize: measuredSize, + pixelRatio: 1.25, + reset: true, + ); + final first = editorController.rendererCubit.state.cameraViewport; + + await editorController.rendererCubit.unbake( + editorController, + bloc.state as DocumentLoadSuccess, + ); + await editorController.rendererCubit.bake( + editorController, + bloc.state as DocumentLoadSuccess, + reset: true, + ); + final second = editorController.rendererCubit.state.cameraViewport; + + expect(second.width, first.width); + expect(second.height, first.height); + expect(second.x, first.x); + expect(second.y, first.y); + expect(second.viewportSize, measuredSize); + expect(second.pixelRatio, 1.25); + }); + test('renderImage does not hide already tracked visible renderers', () async { await bloc.close(); await editorController.close(); From 3eca75131111418f7bc8bf0683d7b9b33d00094c Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 12 Jul 2026 18:06:54 +0200 Subject: [PATCH 061/117] Further improve performance --- app/lib/bloc/document_bloc.dart | 9 ++- app/lib/cubits/editor_renderer.dart | 26 +++++--- app/lib/models/viewport.dart | 2 +- app/lib/models/viewport.freezed.dart | 21 ------- app/lib/renderers/elements/pen.dart | 29 +++++---- app/lib/views/view.dart | 73 +++++++++++++---------- app/test/cubits/editor_renderer_test.dart | 28 +++++++++ app/test/models/viewport_test.dart | 11 ++++ 8 files changed, 125 insertions(+), 74 deletions(-) create mode 100644 app/test/cubits/editor_renderer_test.dart create mode 100644 app/test/models/viewport_test.dart diff --git a/app/lib/bloc/document_bloc.dart b/app/lib/bloc/document_bloc.dart index c6f42761bafd..87aa9647c91d 100644 --- a/app/lib/bloc/document_bloc.dart +++ b/app/lib/bloc/document_bloc.dart @@ -1957,7 +1957,7 @@ class DocumentBloc extends ReplayBloc { final cubit = _editorController; if (state is! DocumentLoadSuccess || cubit == null) return {}; transform ??= cubit.transformCubit.state; - final renderers = cubit.rendererCubit.state.cameraViewport.visibleElements; + final renderers = cubit.rendererCubit.visibleRenderers(rect); if (renderers.isEmpty) return {}; hitElementMode ??= HitElementMode.touchAnywhere; @@ -1991,7 +1991,12 @@ class DocumentBloc extends ReplayBloc { final state = this.state; final cubit = _editorController; if (state is! DocumentLoadSuccess || cubit == null) return {}; - final renderers = cubit.rendererCubit.state.cameraViewport.visibleElements; + if (points.isEmpty) return {}; + var bounds = Rect.fromPoints(points.first, points.first); + for (final point in points.skip(1)) { + bounds = bounds.expandToInclude(Rect.fromPoints(point, point)); + } + final renderers = cubit.rendererCubit.visibleRenderers(bounds); if (renderers.isEmpty) return {}; transform ??= cubit.transformCubit.state; hitElementMode ??= HitElementMode.touchAnywhere; diff --git a/app/lib/cubits/editor_renderer.dart b/app/lib/cubits/editor_renderer.dart index 312750dc1079..a1f0dc0eaf76 100644 --- a/app/lib/cubits/editor_renderer.dart +++ b/app/lib/cubits/editor_renderer.dart @@ -142,13 +142,25 @@ class RendererCubit extends Cubit { void setRendererStates({ Map? rendererStates, Map? temporaryRendererStates, - }) => emit( - state.copyWith( - rendererStates: rendererStates ?? state.rendererStates, - temporaryRendererStates: - temporaryRendererStates ?? state.temporaryRendererStates, - ), - ); + }) { + final nextRendererStates = rendererStates ?? state.rendererStates; + final nextTemporaryRendererStates = + temporaryRendererStates ?? state.temporaryRendererStates; + const equality = MapEquality(); + if (equality.equals(state.rendererStates, nextRendererStates) && + equality.equals( + state.temporaryRendererStates, + nextTemporaryRendererStates, + )) { + return; + } + emit( + state.copyWith( + rendererStates: nextRendererStates, + temporaryRendererStates: nextTemporaryRendererStates, + ), + ); + } List> get renderers => List>.from(state.cameraViewport.bakedElements) diff --git a/app/lib/models/viewport.dart b/app/lib/models/viewport.dart index ba0b2400d3ed..a7aeb905b3ba 100644 --- a/app/lib/models/viewport.dart +++ b/app/lib/models/viewport.dart @@ -13,7 +13,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'viewport.freezed.dart'; -@freezed +@Freezed(equal: false) sealed class CameraViewport with _$CameraViewport { const CameraViewport._(); diff --git a/app/lib/models/viewport.freezed.dart b/app/lib/models/viewport.freezed.dart index 41e7835de6ec..153c8245c792 100644 --- a/app/lib/models/viewport.freezed.dart +++ b/app/lib/models/viewport.freezed.dart @@ -23,14 +23,7 @@ $CameraViewportCopyWith get copyWith => _$CameraViewportCopyWith -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is CameraViewport&&const DeepCollectionEquality().equals(other.backgrounds, backgrounds)&&const DeepCollectionEquality().equals(other.bakedElements, bakedElements)&&const DeepCollectionEquality().equals(other.unbakedElements, unbakedElements)&&const DeepCollectionEquality().equals(other.visibleElements, visibleElements)&&const DeepCollectionEquality().equals(other.visibleUnbakedElements, visibleUnbakedElements)&&(identical(other.width, width) || other.width == width)&&(identical(other.height, height) || other.height == height)&&(identical(other.viewportSize, viewportSize) || other.viewportSize == viewportSize)&&(identical(other.pixelRatio, pixelRatio) || other.pixelRatio == pixelRatio)&&(identical(other.scale, scale) || other.scale == scale)&&(identical(other.x, x) || other.x == x)&&(identical(other.y, y) || other.y == y)&&(identical(other.resolution, resolution) || other.resolution == resolution)&&const DeepCollectionEquality().equals(other.rendererStates, rendererStates)&&const DeepCollectionEquality().equals(other.invisibleLayers, invisibleLayers)&&(identical(other.image, image) || other.image == image)&&(identical(other.belowLayerImage, belowLayerImage) || other.belowLayerImage == belowLayerImage)&&(identical(other.aboveLayerImage, aboveLayerImage) || other.aboveLayerImage == aboveLayerImage)); -} - -@override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(backgrounds),const DeepCollectionEquality().hash(bakedElements),const DeepCollectionEquality().hash(unbakedElements),const DeepCollectionEquality().hash(visibleElements),const DeepCollectionEquality().hash(visibleUnbakedElements),width,height,viewportSize,pixelRatio,scale,x,y,resolution,const DeepCollectionEquality().hash(rendererStates),const DeepCollectionEquality().hash(invisibleLayers),image,belowLayerImage,aboveLayerImage); @override String toString() { @@ -166,14 +159,7 @@ $CameraViewportUnbakedCopyWith get copyWith => _$CameraVi -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is CameraViewportUnbaked&&const DeepCollectionEquality().equals(other._backgrounds, _backgrounds)&&const DeepCollectionEquality().equals(other._bakedElements, _bakedElements)&&const DeepCollectionEquality().equals(other._unbakedElements, _unbakedElements)&&const DeepCollectionEquality().equals(other._visibleElements, _visibleElements)&&const DeepCollectionEquality().equals(other._visibleUnbakedElements, _visibleUnbakedElements)&&(identical(other.width, width) || other.width == width)&&(identical(other.height, height) || other.height == height)&&(identical(other.viewportSize, viewportSize) || other.viewportSize == viewportSize)&&(identical(other.pixelRatio, pixelRatio) || other.pixelRatio == pixelRatio)&&(identical(other.scale, scale) || other.scale == scale)&&(identical(other.x, x) || other.x == x)&&(identical(other.y, y) || other.y == y)&&(identical(other.resolution, resolution) || other.resolution == resolution)&&const DeepCollectionEquality().equals(other._rendererStates, _rendererStates)&&const DeepCollectionEquality().equals(other._invisibleLayers, _invisibleLayers)&&(identical(other.image, image) || other.image == image)&&(identical(other.belowLayerImage, belowLayerImage) || other.belowLayerImage == belowLayerImage)&&(identical(other.aboveLayerImage, aboveLayerImage) || other.aboveLayerImage == aboveLayerImage)); -} - -@override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_backgrounds),const DeepCollectionEquality().hash(_bakedElements),const DeepCollectionEquality().hash(_unbakedElements),const DeepCollectionEquality().hash(_visibleElements),const DeepCollectionEquality().hash(_visibleUnbakedElements),width,height,viewportSize,pixelRatio,scale,x,y,resolution,const DeepCollectionEquality().hash(_rendererStates),const DeepCollectionEquality().hash(_invisibleLayers),image,belowLayerImage,aboveLayerImage); @override String toString() { @@ -308,14 +294,7 @@ $CameraViewportBakedCopyWith get copyWith => _$CameraViewpo -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is CameraViewportBaked&&const DeepCollectionEquality().equals(other._backgrounds, _backgrounds)&&(identical(other.image, image) || other.image == image)&&(identical(other.belowLayerImage, belowLayerImage) || other.belowLayerImage == belowLayerImage)&&(identical(other.aboveLayerImage, aboveLayerImage) || other.aboveLayerImage == aboveLayerImage)&&(identical(other.width, width) || other.width == width)&&(identical(other.height, height) || other.height == height)&&(identical(other.viewportSize, viewportSize) || other.viewportSize == viewportSize)&&(identical(other.pixelRatio, pixelRatio) || other.pixelRatio == pixelRatio)&&const DeepCollectionEquality().equals(other._bakedElements, _bakedElements)&&const DeepCollectionEquality().equals(other._unbakedElements, _unbakedElements)&&const DeepCollectionEquality().equals(other._visibleElements, _visibleElements)&&const DeepCollectionEquality().equals(other._visibleUnbakedElements, _visibleUnbakedElements)&&(identical(other.scale, scale) || other.scale == scale)&&(identical(other.x, x) || other.x == x)&&(identical(other.resolution, resolution) || other.resolution == resolution)&&(identical(other.y, y) || other.y == y)&&const DeepCollectionEquality().equals(other._rendererStates, _rendererStates)&&const DeepCollectionEquality().equals(other._invisibleLayers, _invisibleLayers)); -} - -@override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_backgrounds),image,belowLayerImage,aboveLayerImage,width,height,viewportSize,pixelRatio,const DeepCollectionEquality().hash(_bakedElements),const DeepCollectionEquality().hash(_unbakedElements),const DeepCollectionEquality().hash(_visibleElements),const DeepCollectionEquality().hash(_visibleUnbakedElements),scale,x,resolution,y,const DeepCollectionEquality().hash(_rendererStates),const DeepCollectionEquality().hash(_invisibleLayers)); @override String toString() { diff --git a/app/lib/renderers/elements/pen.dart b/app/lib/renderers/elements/pen.dart index dd48eec5e9d3..b8ad08ba31d9 100644 --- a/app/lib/renderers/elements/pen.dart +++ b/app/lib/renderers/elements/pen.dart @@ -67,9 +67,13 @@ class PenRenderer extends Renderer { } bool shouldSimulatePressure() { - final points = element.points.sublist(1); - var pressure = points.firstOrNull?.pressure ?? 0; - return points.every((element) => element.pressure == pressure); + final points = element.points; + if (points.length < 2) return true; + final pressure = points[1].pressure; + for (var i = 2; i < points.length; i++) { + if (points[i].pressure != pressure) return false; + } + return true; } @override @@ -100,13 +104,18 @@ class PenRenderer extends Renderer { bottomRightCorner.dx, bottomRightCorner.dy, ); - final center = Rect.fromPoints(topLeftCorner, bottomRightCorner).center; - final rotatedPoints = points - .map((e) => e.rotate(center, rotation / 180 * pi)) - .toList(); - topLeftCorner = rotatedPoints.first.toOffset(); - bottomRightCorner = rotatedPoints.first.toOffset(); - for (final element in rotatedPoints) { + final rotationCenter = Rect.fromPoints( + topLeftCorner, + bottomRightCorner, + ).center; + final pointsForBounds = rotation == 0 + ? points + : points + .map((e) => e.rotate(rotationCenter, rotation / 180 * pi)) + .toList(); + topLeftCorner = pointsForBounds.first.toOffset(); + bottomRightCorner = pointsForBounds.first.toOffset(); + for (final element in pointsForBounds) { final width = property.strokeWidth + element.pressure * property.thinning; topLeftCorner = Offset( min(topLeftCorner.dx, element.x - width), diff --git a/app/lib/views/view.dart b/app/lib/views/view.dart index 2e8f1004a407..a32f1297820a 100644 --- a/app/lib/views/view.dart +++ b/app/lib/views/view.dart @@ -549,16 +549,9 @@ class _MainViewViewportState extends State builder: (context, rendererState) { return BlocBuilder( buildWhen: (previous, current) => - previous.foregrounds != current.foregrounds || previous.handler != current.handler || previous.temporaryHandler != current.temporaryHandler || - previous.toggleableForegrounds != - current.toggleableForegrounds || - previous.temporaryForegrounds != - current.temporaryForegrounds || - previous.networkingForegrounds != - current.networkingForegrounds || previous.cursor != current.cursor || previous.temporaryCursor != current.temporaryCursor, builder: (context, toolState) { @@ -902,8 +895,6 @@ class _MainViewViewportState extends State _handlePointerCancel(event, cubit), child: _buildCanvas( rendererState, - toolState, - cubit, state, delayBake, ), @@ -929,8 +920,6 @@ class _MainViewViewportState extends State Widget _buildCanvas( RendererRuntimeState rendererState, - ToolRuntimeState toolState, - EditorController cubit, DocumentLoaded state, VoidCallback delayBake, ) { @@ -984,30 +973,48 @@ class _MainViewViewportState extends State return Stack( children: [ Container(color: ColorScheme.of(context).surfaceDim), - CustomPaint( - size: Size.infinite, - foregroundPainter: ForegroundPainter( - toolState.getAllForegrounds(), - state.data, - state.page, - state.info, - ColorScheme.of(context), - frictionTransform, - toolState.selection, - state.settingsCubit.state.navigatorPosition, + RepaintBoundary( + child: CustomPaint( + size: Size.infinite, + painter: ViewPainter( + state.data, + state.page, + state.info, + cameraViewport: rendererState.cameraViewport, + transform: frictionTransform, + invisibleLayers: state.invisibleLayers, + currentArea: state.currentArea, + colorScheme: ColorScheme.of(context), + ), + isComplex: true, ), - painter: ViewPainter( - state.data, - state.page, - state.info, - cameraViewport: rendererState.cameraViewport, - transform: frictionTransform, - invisibleLayers: state.invisibleLayers, - currentArea: state.currentArea, - colorScheme: ColorScheme.of(context), + ), + BlocBuilder( + buildWhen: (previous, current) => + previous.foregrounds != current.foregrounds || + previous.temporaryForegrounds != + current.temporaryForegrounds || + previous.toggleableForegrounds != + current.toggleableForegrounds || + previous.networkingForegrounds != + current.networkingForegrounds || + previous.selection != current.selection, + builder: (context, toolState) => RepaintBoundary( + child: CustomPaint( + size: Size.infinite, + painter: ForegroundPainter( + toolState.getAllForegrounds(), + state.data, + state.page, + state.info, + ColorScheme.of(context), + frictionTransform, + toolState.selection, + state.settingsCubit.state.navigatorPosition, + ), + willChange: true, + ), ), - isComplex: true, - willChange: true, ), ], ); diff --git a/app/test/cubits/editor_renderer_test.dart b/app/test/cubits/editor_renderer_test.dart new file mode 100644 index 000000000000..99ab378b488c --- /dev/null +++ b/app/test/cubits/editor_renderer_test.dart @@ -0,0 +1,28 @@ +import 'package:butterfly/cubits/editor_controller.dart'; +import 'package:butterfly/cubits/settings.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('equivalent renderer states do not emit a runtime update', () async { + SharedPreferences.setMockInitialValues({}); + final preferences = await SharedPreferences.getInstance(); + final cubit = RendererCubit(SettingsCubit(preferences)); + final emissions = []; + final subscription = cubit.stream.listen(emissions.add); + + cubit.setRendererStates( + rendererStates: const {'element': RendererState.visible}, + ); + cubit.setRendererStates( + rendererStates: const {'element': RendererState.visible}, + ); + await Future.delayed(Duration.zero); + + expect(emissions, hasLength(1)); + await subscription.cancel(); + await cubit.close(); + }); +} diff --git a/app/test/models/viewport_test.dart b/app/test/models/viewport_test.dart new file mode 100644 index 000000000000..ee85b7f6849d --- /dev/null +++ b/app/test/models/viewport_test.dart @@ -0,0 +1,11 @@ +import 'package:butterfly/models/viewport.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('viewport snapshots use identity equality', () { + const viewport = CameraViewport.unbaked(width: 100, height: 100); + + expect(viewport.copyWith(), isNot(equals(viewport))); + expect(viewport, equals(viewport)); + }); +} From 84f57898408950c10709518f21b6d122faddaeb8 Mon Sep 17 00:00:00 2001 From: CodeDoctor Date: Mon, 13 Jul 2026 11:47:12 +0200 Subject: [PATCH 062/117] New Crowdin updates (#1160) * New translations app_en.arb (Romanian) [ci skip] [ci skip] * New translations app_en.arb (French) [ci skip] [ci skip] * New translations app_en.arb (Spanish) [ci skip] [ci skip] * New translations app_en.arb (Arabic) [ci skip] [ci skip] * New translations app_en.arb (Czech) [ci skip] [ci skip] * New translations app_en.arb (Danish) [ci skip] [ci skip] * New translations app_en.arb (German) [ci skip] [ci skip] * New translations app_en.arb (Greek) [ci skip] [ci skip] * New translations app_en.arb (Finnish) [ci skip] [ci skip] * New translations app_en.arb (Italian) [ci skip] [ci skip] * New translations app_en.arb (Japanese) [ci skip] [ci skip] * New translations app_en.arb (Dutch) [ci skip] [ci skip] * New translations app_en.arb (Norwegian) [ci skip] [ci skip] * New translations app_en.arb (Polish) [ci skip] [ci skip] * New translations app_en.arb (Portuguese) [ci skip] [ci skip] * New translations app_en.arb (Russian) [ci skip] [ci skip] * New translations app_en.arb (Swedish) [ci skip] [ci skip] * New translations app_en.arb (Ukrainian) [ci skip] [ci skip] * New translations app_en.arb (Chinese Simplified) [ci skip] [ci skip] * New translations app_en.arb (Portuguese, Brazilian) [ci skip] [ci skip] * New translations app_en.arb (Indonesian) [ci skip] [ci skip] * New translations app_en.arb (Afrikaans) [ci skip] [ci skip] * New translations app_en.arb (Catalan) [ci skip] [ci skip] * New translations app_en.arb (Hebrew) [ci skip] [ci skip] * New translations app_en.arb (Hungarian) [ci skip] [ci skip] * New translations app_en.arb (Korean) [ci skip] [ci skip] * New translations app_en.arb (Turkish) [ci skip] [ci skip] * New translations app_en.arb (Chinese Traditional) [ci skip] [ci skip] * New translations app_en.arb (Vietnamese) [ci skip] [ci skip] * New translations app_en.arb (Thai) [ci skip] [ci skip] * New translations app_en.arb (Hindi) [ci skip] [ci skip] * New translations app_en.arb (Odia) [ci skip] [ci skip] * New translations app_en.arb (Serbian) [ci skip] [ci skip] * New translations app_en.arb (Romanian) [ci skip] [ci skip] * New translations app_en.arb (French) [ci skip] [ci skip] * New translations app_en.arb (Spanish) [ci skip] [ci skip] * New translations app_en.arb (Arabic) [ci skip] [ci skip] * New translations app_en.arb (Czech) [ci skip] [ci skip] * New translations app_en.arb (Danish) [ci skip] [ci skip] * New translations app_en.arb (German) [ci skip] [ci skip] * New translations app_en.arb (Greek) [ci skip] [ci skip] * New translations app_en.arb (Finnish) [ci skip] [ci skip] * New translations app_en.arb (Italian) [ci skip] [ci skip] * New translations app_en.arb (Japanese) [ci skip] [ci skip] * New translations app_en.arb (Dutch) [ci skip] [ci skip] * New translations app_en.arb (Norwegian) [ci skip] [ci skip] * New translations app_en.arb (Polish) [ci skip] [ci skip] * New translations app_en.arb (Portuguese) [ci skip] [ci skip] * New translations app_en.arb (Russian) [ci skip] [ci skip] * New translations app_en.arb (Swedish) [ci skip] [ci skip] * New translations app_en.arb (Ukrainian) [ci skip] [ci skip] * New translations app_en.arb (Chinese Simplified) [ci skip] [ci skip] * New translations app_en.arb (Portuguese, Brazilian) [ci skip] [ci skip] * New translations app_en.arb (Indonesian) [ci skip] [ci skip] * New translations app_en.arb (Afrikaans) [ci skip] [ci skip] * New translations app_en.arb (Catalan) [ci skip] [ci skip] * New translations app_en.arb (Hebrew) [ci skip] [ci skip] * New translations app_en.arb (Hungarian) [ci skip] [ci skip] * New translations app_en.arb (Korean) [ci skip] [ci skip] * New translations app_en.arb (Turkish) [ci skip] [ci skip] * New translations app_en.arb (Chinese Traditional) [ci skip] [ci skip] * New translations app_en.arb (Vietnamese) [ci skip] [ci skip] * New translations app_en.arb (Thai) [ci skip] [ci skip] * New translations app_en.arb (Hindi) [ci skip] [ci skip] * New translations app_en.arb (Odia) [ci skip] [ci skip] * New translations app_en.arb (Serbian) [ci skip] [ci skip] * New translations app_en.arb (Romanian) [ci skip] [ci skip] * New translations app_en.arb (French) [ci skip] [ci skip] * New translations app_en.arb (Spanish) [ci skip] [ci skip] * New translations app_en.arb (Arabic) [ci skip] [ci skip] * New translations app_en.arb (Czech) [ci skip] [ci skip] * New translations app_en.arb (Danish) [ci skip] [ci skip] * New translations app_en.arb (German) [ci skip] [ci skip] * New translations app_en.arb (Greek) [ci skip] [ci skip] * New translations app_en.arb (Finnish) [ci skip] [ci skip] * New translations app_en.arb (Italian) [ci skip] [ci skip] * New translations app_en.arb (Japanese) [ci skip] [ci skip] * New translations app_en.arb (Dutch) [ci skip] [ci skip] * New translations app_en.arb (Norwegian) [ci skip] [ci skip] * New translations app_en.arb (Polish) [ci skip] [ci skip] * New translations app_en.arb (Portuguese) [ci skip] [ci skip] * New translations app_en.arb (Russian) [ci skip] [ci skip] * New translations app_en.arb (Swedish) [ci skip] [ci skip] * New translations app_en.arb (Ukrainian) [ci skip] [ci skip] * New translations app_en.arb (Chinese Simplified) [ci skip] [ci skip] * New translations app_en.arb (Portuguese, Brazilian) [ci skip] [ci skip] * New translations app_en.arb (Indonesian) [ci skip] [ci skip] * New translations app_en.arb (Afrikaans) [ci skip] [ci skip] * New translations app_en.arb (Catalan) [ci skip] [ci skip] * New translations app_en.arb (Hebrew) [ci skip] [ci skip] * New translations app_en.arb (Hungarian) [ci skip] [ci skip] * New translations app_en.arb (Korean) [ci skip] [ci skip] * New translations app_en.arb (Turkish) [ci skip] [ci skip] * New translations app_en.arb (Chinese Traditional) [ci skip] [ci skip] * New translations app_en.arb (Vietnamese) [ci skip] [ci skip] * New translations app_en.arb (Thai) [ci skip] [ci skip] * New translations app_en.arb (Hindi) [ci skip] [ci skip] * New translations app_en.arb (Odia) [ci skip] [ci skip] * New translations app_en.arb (Serbian) [ci skip] [ci skip] --- app/lib/l10n/app_af.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_ar.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_ca.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_cs.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_da.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_de.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_el.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_es.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_fi.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_fr.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_he.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_hi.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_hu.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_id.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_it.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_ja.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_ko.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_nl.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_no.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_or.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_pl.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_pt.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_pt_BR.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_ro.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_ru.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_sr.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_sv.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_th.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_tr.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_uk.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_vi.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_zh-Hant.arb | 136 +++++++++++++++++++++++++++++++++-- app/lib/l10n/app_zh.arb | 136 +++++++++++++++++++++++++++++++++-- 33 files changed, 4356 insertions(+), 132 deletions(-) diff --git a/app/lib/l10n/app_af.arb b/app/lib/l10n/app_af.arb index 796c9c510a48..8d2ed1b9bfab 100644 --- a/app/lib/l10n/app_af.arb +++ b/app/lib/l10n/app_af.arb @@ -11,7 +11,13 @@ "systemTheme": "Gebruik verstek stelseltema", "view": "Aansig", "contentViewport": "Content Viewport", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Limit Viewport to positive coordinates", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Off", "canvas": "Canvas", "interface": "Interface", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Sensitiwiteit", - "sensitivityHint": "Hoe hoër die waarde, hoe sensitiewer die invoer", "horizontal": "Horisontaal", "vertical": "Vertikaal", "plain": "Eenvoudig", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Dokument", + "documentStates": "Document states", "camera": "Kamera", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Onder links", "bottomRight": "Onder regs", "zoomPosition": "Zoom control position", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Kasse", "manage": "Bestuur", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Invoergebare", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Inheemse titelbalk", "mode": "Mode", "syncMode": "Sinkroniseer-modus", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Geen mobiel", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Handmatig", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Soek", "@search": { "description": "Search action" }, "properties": "Eienskappe", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Speld vas", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Direction", "onlyAvailableLargerScreen": "Slegs beskikbaar op groter skerms", "toolbarPosition": "Nutsbalkposisie", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Roteer", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Navigasiereling", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Knip", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Platformtema", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Werkskerm", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "ICE-bedieners", "collaboration": "Samewerking", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Web Sok", "iceServer": "ICE-bediener", @@ -1037,7 +1065,7 @@ "hideUI": "Versteek UI", "density": "Digtheid", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Kompak", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Gaan in elk geval voort", "zoomControl": "Zoembeheer", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Hoë kontras", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Hierdie waarde moet 'n geldige nommer wees", "createAreas": "Skep areas", "autosave": "Outostoor", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Keer om", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Nutsbalkgrootte", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Voeg alles by", "onlyCurrentPage": "Slegs huidige bladsy", "smoothNavigation": "Gladde navigasie", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Edge pan area switching", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Use Android SAF", "exact": "Presies", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Nutsbalkrye", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Wysertoets", "pressure": "Druk", "small": "Klein", @@ -1158,6 +1207,9 @@ "selectAll": "Kies alles", "overrideTools": "Oorskryf gereedskap", "hideCursorWhileDrawing": "Versteek wyser tydens teken", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Geïnstalleer", "install": "Installeer", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "By opstart", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Tuisskerm", "lastNote": "Laaste nota", "newNote": "Nuwe nota", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignore pressure", - "ignoreFirstPressureDescription": "On some devices, the first pressure value is not accurate. This setting will ignore the first pressure value and use the pressure of the second event instead.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Temporary", "simpleToolbarVisibility": "Simple toolbar visibility", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Autosave delay", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Saved", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Bring moved elements to front", "addTool": "Add tool", "nextPage": "Next page", - "previousPage": "Previous page" + "previousPage": "Previous page", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Current page", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_ar.arb b/app/lib/l10n/app_ar.arb index 86e55e452c13..19300a85f0fb 100644 --- a/app/lib/l10n/app_ar.arb +++ b/app/lib/l10n/app_ar.arb @@ -11,7 +11,13 @@ "systemTheme": "استخدام سمة النظام الافتراضية", "view": "العرض", "contentViewport": "نافذة عرض المحتوى", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "قيّد نافذة العرض بالإحداثيات الإيجابية", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "معطل", "canvas": "اللوحة", "interface": "الواجهة", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "الحساسية", - "sensitivityHint": "ارتفاع القيمة، يزيد حساسية الإدخال", "horizontal": "أفقي", "vertical": "عمودي", "plain": "سادة", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "مستند", + "documentStates": "Document states", "camera": "كاميرا", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "أسفل اليسار", "bottomRight": "أسفل اليمين", "zoomPosition": "موقع أداة التكبير", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "المخبآت", "manage": "إدارة", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "إيماءات الإدخال", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "استخدم شريط عنوان النظام", "mode": "Mode", "syncMode": "وضع المزامنة", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "لا تستخدم الاتصال الخلوي", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "يدوي", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "البحث", "@search": { "description": "Search action" }, "properties": "الخصائص", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "تثبيت", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "الاتجاه", "onlyAvailableLargerScreen": "متوفر فقط على شاشات أكبر", "toolbarPosition": "موضع شريط الأدوات", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "تدوير", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "سكة التوجيه", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "قصّ", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "مظهر النظام", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "سطح المكتب", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "خوادم ICE", "collaboration": "التعاون", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "مقبس الويب", "iceServer": "خادم ICE", @@ -1037,7 +1065,7 @@ "hideUI": "إخفاء الواجهة", "density": "الكثافة", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "مضغوط", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "المتابعة على أي حال", "zoomControl": "تحكمات التكبير و التصغير", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "عرض الصور المُصغّرة", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "تباين عالي", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "يجب أن تكون هذه القيمة رقماً صحيحاً", "createAreas": "إنشاء مناطق", "autosave": "حفظ تلقائي", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "عكس", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "حجم شريط الأدوات", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "إضافة الكل", "onlyCurrentPage": "الصفحة الحالية فقط", "smoothNavigation": "التنقل السلس", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "تبديل منطقة الشريط", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "استخدام أندرويد SAF", "exact": "بالضبط", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "صفوف شريط الأدوات", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "اختبار المؤشر", "pressure": "الضغط", "small": "صغير", @@ -1158,6 +1207,9 @@ "selectAll": "حدد الكل", "overrideTools": "استبدال اﻷدوات", "hideCursorWhileDrawing": "إخفاء المؤشر أثناء الرسم", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "مثبت", "install": "تثبيت", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "عند بدء التشغيل", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "الشاشة الرئيسة", "lastNote": "الملاحظة الأخيرة", "newNote": "ملاحظة جديدة", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "تجاهل الضغط", - "ignoreFirstPressureDescription": "في بعض الأجهزة، قيمة الضغط الأولى غير دقيقة. سيتجاهل هذا الإعداد قيمة الضغط الأولى ويستخدم ضغط الحدث الثاني بدلاً منها.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "مؤقت", "simpleToolbarVisibility": "عرض شريط الأدوات البسيط", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "تأخر الحفظ التلقائي", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "محفوظ", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "إحضار العناصر المنقولة إلى المقدمة", "addTool": "إضافة أداة", "nextPage": "الصفحة التالية", - "previousPage": "الصفحة السابقة" + "previousPage": "الصفحة السابقة", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "الصفحة الحالية", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_ca.arb b/app/lib/l10n/app_ca.arb index 8818a4b5d2e0..0370dc5be390 100644 --- a/app/lib/l10n/app_ca.arb +++ b/app/lib/l10n/app_ca.arb @@ -11,7 +11,13 @@ "systemTheme": "Utilitzar tema del sistema per defecte", "view": "Veure", "contentViewport": "Content Viewport", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Limit Viewport to positive coordinates", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Off", "canvas": "Canvas", "interface": "Interface", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Sensibilitat", - "sensitivityHint": "Com més alt el valor, més sensible serà l'entrada", "horizontal": "Horitzontal", "vertical": "Vertical", "plain": "Pla", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Document", + "documentStates": "Document states", "camera": "Càmera", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Inferior esquerra", "bottomRight": "Inferior dreta", "zoomPosition": "Zoom control position", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Cachés", "manage": "Gestiona", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Gestos d'entrada", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Barra de títol nativa", "mode": "Mode", "syncMode": "Mode de sincronització", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Sense mòbil", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Manual", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Cerca", "@search": { "description": "Search action" }, "properties": "Propietats", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Fixar", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Direction", "onlyAvailableLargerScreen": "Només disponible en pantalles grans", "toolbarPosition": "Posició de la barra d'eines", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Rota", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Rail de navegació", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Talla", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Tema de plataforma", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Escriptori", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "Servidors ICE", "collaboration": "Col·laboració", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Web Socket", "iceServer": "Servidor ICE", @@ -1037,7 +1065,7 @@ "hideUI": "Amaga la interfície", "density": "Densitat", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Compacte", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Continua igualment", "zoomControl": "Control de zoom", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Alt contrast", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Aquest valor ha de ser un nombre vàlid", "createAreas": "Crea àrees", "autosave": "Desament automàtic", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Inverteix", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Mida de la barra d'eines", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Afegeix-ho tot", "onlyCurrentPage": "Només pàgina actual", "smoothNavigation": "Navegació fluida", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Edge pan area switching", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Use Android SAF", "exact": "Exacte", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Files de barra d'eines", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Prova de punter", "pressure": "Pressió", "small": "Petit", @@ -1158,6 +1207,9 @@ "selectAll": "Selecciona-ho tot", "overrideTools": "Sobreescriu eines", "hideCursorWhileDrawing": "Amaga el cursor mentre dibuixes", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Instal·lat", "install": "Instal·la", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "A l'inici", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Pantalla d'inici", "lastNote": "Darrera nota", "newNote": "Nova nota", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignore pressure", - "ignoreFirstPressureDescription": "On some devices, the first pressure value is not accurate. This setting will ignore the first pressure value and use the pressure of the second event instead.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Temporary", "simpleToolbarVisibility": "Simple toolbar visibility", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Autosave delay", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Saved", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Bring moved elements to front", "addTool": "Add tool", "nextPage": "Next page", - "previousPage": "Previous page" + "previousPage": "Previous page", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Current page", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_cs.arb b/app/lib/l10n/app_cs.arb index c2f0a356c893..d787d90411bb 100644 --- a/app/lib/l10n/app_cs.arb +++ b/app/lib/l10n/app_cs.arb @@ -11,7 +11,13 @@ "systemTheme": "Použít výchozí motiv systému", "view": "Zobrazit", "contentViewport": "Zobrazení obsahu", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Omezit zobrazení na kladné souřadnice", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Vypnuto", "canvas": "Plátno", "interface": "Rozhraní", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Citlivost", - "sensitivityHint": "Čím vyšší hodnota, tím citlivější je vstup", "horizontal": "Horizontální", "vertical": "Vertikální", "plain": "Jednoduchý", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Dokument", + "documentStates": "Document states", "camera": "Fotoaparát", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Vlevo dole", "bottomRight": "Vpravo dole", "zoomPosition": "Poloha ovládání přiblížení", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Cache", "manage": "Spravovat", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Gesta", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Nativní titulek", "mode": "Mode", "syncMode": "Režim synchronizace", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Žádný mobil", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Ruční", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Hledat", "@search": { "description": "Search action" }, "properties": "Vlastnosti", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Připnout", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Směr", "onlyAvailableLargerScreen": "Dostupné pouze na větších obrazovkách", "toolbarPosition": "Pozice panelu nástrojů", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Otočit", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Navigační kolejnice", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Vyjmout", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Motiv platformy", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Stolní počítače", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "Servery ICE", "collaboration": "Spolupráce", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Webová raketa", "iceServer": "Server ICE", @@ -1037,7 +1065,7 @@ "hideUI": "Skrýt uživatelské rozhraní", "density": "Hustota", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Kompaktní", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Přesto pokračovat", "zoomControl": "Ovládání přiblížení", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Vysoký kontrast", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Tato hodnota by měla být platné číslo", "createAreas": "Vytvořit oblasti", "autosave": "Automatické uložení", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Převrátit", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Velikost panelu nástrojů", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Přidat vše", "onlyCurrentPage": "Pouze aktuální stránka", "smoothNavigation": "Hladká navigace", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Přepínání prostoru skořepiny hrany", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Použít Android SAF", "exact": "Přesné", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Řádky panelu nástrojů", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Zkouška ukazovatele", "pressure": "Tlak", "small": "Malá", @@ -1158,6 +1207,9 @@ "selectAll": "Vybrat vše", "overrideTools": "Přepsat nástroje", "hideCursorWhileDrawing": "Skrýt kurzor při kreslení", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Nainstalováno", "install": "Instalovat", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "Při startu", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Domovská stránka", "lastNote": "Poslední poznámka", "newNote": "Nová poznámka", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignorovat sílu stisku", - "ignoreFirstPressureDescription": "U některých zařízení není první zaznamenaná hodnota tlaku přesná. Toto nastavení bude ignorovat první hodnotu tlaku a místo toho použije druhou.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Dočasné", "simpleToolbarVisibility": "Jednoduchá viditelnost panelu nástrojů", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Prodleva automatického ukládání", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Uloženo", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Přeneste přesunuté prvky do předku", "addTool": "Přidat nástroj", "nextPage": "Další stránka", - "previousPage": "Předchozí stránka" + "previousPage": "Předchozí stránka", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Aktuální stránka", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_da.arb b/app/lib/l10n/app_da.arb index 5f425955b052..e057956a96ca 100644 --- a/app/lib/l10n/app_da.arb +++ b/app/lib/l10n/app_da.arb @@ -11,7 +11,13 @@ "systemTheme": "Brug standard-systemtema", "view": "Vis", "contentViewport": "Indholdsvisning", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Begræns visning til positive koordinater", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Fra", "canvas": "Lærred", "interface": "Grænseflade", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Følsomhed", - "sensitivityHint": "Jo højere værdi, jo mere følsom input", "horizontal": "Horisontal", "vertical": "Lodret", "plain": "Enkelt", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Dokument", + "documentStates": "Document states", "camera": "Kamera", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Nederst til venstre", "bottomRight": "Nederst til højre", "zoomPosition": "Zoom kontrolposition", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Cacher", "manage": "Administrer", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Input gestures", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Lokal titellinje", "mode": "Mode", "syncMode": "Synkroniser tilstand", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Ingen mobil", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Manuelt", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Søg", "@search": { "description": "Search action" }, "properties": "Egenskaber", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Fastgør", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Retning", "onlyAvailableLargerScreen": "Kun tilgængelig på større skærme", "toolbarPosition": "Værktøjslinjens position", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Rotér", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Navigation jernbane", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Klip", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Platform tema", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Skrivebord", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "ICE Servere", "collaboration": "Samarbejde", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Web Sokkel", "iceServer": "Ic- Server", @@ -1037,7 +1065,7 @@ "hideUI": "Skjul Brugerflade", "density": "Tæthed", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Kompakt", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Fortsæt alligevel", "zoomControl": "Zoom kontrol", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Høj kontrast", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Denne værdi skal være gyldigt nummer", "createAreas": "Opret områder", "autosave": "Autosave", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Invert", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Værktøjslinjens størrelse", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Tilføj alle", "onlyCurrentPage": "Kun nuværende side", "smoothNavigation": "Glat navigation", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Kant panorering område skifte", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Brug Android SAF", "exact": "Præcis", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Værktøjslinje- rækker", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Markør test", "pressure": "Tryk", "small": "Lille", @@ -1158,6 +1207,9 @@ "selectAll": "Vælg alle", "overrideTools": "Tilsidesæt værktøjer", "hideCursorWhileDrawing": "Skjul markør under tegning", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Installeret", "install": "Installér", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "Ved opstart", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Startskærm", "lastNote": "Sidste note", "newNote": "Ny note", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignorér tryk", - "ignoreFirstPressureDescription": "På nogle enheder er førstetryksværdien ikke nøjagtig. Denne indstilling vil ignorere den første trykværdi og bruge trykket på den anden begivenhed i stedet.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Midlertidig", "simpleToolbarVisibility": "Simpel synlighed for værktøjslinjen", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Autogem forsinkelse", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Gemt", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Bring flyttede elementer foran", "addTool": "Tilføj værktøj", "nextPage": "Næste side", - "previousPage": "Forrige side" + "previousPage": "Forrige side", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Aktuel side", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_de.arb b/app/lib/l10n/app_de.arb index 838e486c71c2..0c823896a60d 100644 --- a/app/lib/l10n/app_de.arb +++ b/app/lib/l10n/app_de.arb @@ -11,7 +11,13 @@ "systemTheme": "Systemdesign verwenden", "view": "Ansicht", "contentViewport": "Inhalts-Viewport", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Viewport auf positive Koordinaten beschränken", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Aus", "canvas": "Leinwand", "interface": "Schnittstelle", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Empfindlichkeit", - "sensitivityHint": "Je höher der Wert, desto sensibler die Eingabe", "horizontal": "Horizontal", "vertical": "Vertikal", "plain": "Schlicht", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Dokument", + "documentStates": "Document states", "camera": "Kamera", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Unten links", "bottomRight": "Unten rechts", "zoomPosition": "Position der Zoomkontrolle", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Caches", "manage": "Verwalten", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Eingabegesten", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Native Titelleiste", "mode": "Modus", "syncMode": "Sync-Modus", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Nicht mobil", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Manuell", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Suchen", "@search": { "description": "Search action" }, "properties": "Eigenschaften", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Anheften", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Richtung", "onlyAvailableLargerScreen": "Nur auf größeren Bildschirmen verfügbar", "toolbarPosition": "Position der Werkzeugleiste", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Drehen", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Navigationsschiene", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Ausschneiden", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Plattformdesign", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Desktop", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "ICE-Server", "collaboration": "Zusammenarbeit", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "WebRTC", "webSocket": "Web-Socket", "iceServer": "ICE-Server", @@ -1037,7 +1065,7 @@ "hideUI": "UI ausblenden", "density": "Dichte", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Kompakt", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Trotzdem fortfahren", "zoomControl": "Zoom-Steuerung", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Thumbnails anzeigen", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Hoher Kontrast", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Dieser Wert sollte eine gültige Zahl sein", "createAreas": "Bereiche erstellen", "autosave": "Automatisches Speichern", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Umkehren", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Größe der Werkzeugleiste", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Alle hinzufügen", "onlyCurrentPage": "Nur aktuelle Seite", "smoothNavigation": "Glatte Navigation", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Kantenschaltfläche umschalten", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Android SAF verwenden", "exact": "Exakt", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Zeilen der Symbolleiste", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Zeigertest", "pressure": "Druck", "small": "Klein", @@ -1158,6 +1207,9 @@ "selectAll": "Alles auswählen", "overrideTools": "Werkzeuge überschreiben", "hideCursorWhileDrawing": "Cursor während Zeichnung ausblenden", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Installiert", "install": "Installieren", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "Beim Start", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Startbildschirm", "lastNote": "Letzte Notiz", "newNote": "Neue Notiz", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignoriere Druck", - "ignoreFirstPressureDescription": "Bei einigen Geräten ist der erste Druckwert nicht korrekt. Diese Einstellung ignoriert den ersten Druckwert und verwendet stattdessen den Druck des zweiten Ereignisses.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Temporär", "simpleToolbarVisibility": "Einfache Symbolleiste Sichtbarkeit", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Verzögerung für automatisches Speichern", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Gespeichert", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Bewege Elemente nach vorne", "addTool": "Werkzeug hinzufügen", "nextPage": "Nächste Seite", - "previousPage": "Vorherige Seite" + "previousPage": "Vorherige Seite", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Aktuelle Seite", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_el.arb b/app/lib/l10n/app_el.arb index 610f5230ab7c..e49f4358c406 100644 --- a/app/lib/l10n/app_el.arb +++ b/app/lib/l10n/app_el.arb @@ -11,7 +11,13 @@ "systemTheme": "Χρήση προεπιλεγμένου θέματος συστήματος", "view": "Προβολή", "contentViewport": "Προβολή Περιεχομένου", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Περιορισμός της προβολής στις θετικές συντεταγμένες", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Ανενεργό", "canvas": "Καμβάς", "interface": "Διεπαφή", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Ευαισθησία", - "sensitivityHint": "Όσο υψηλότερη είναι η τιμή, τόσο πιο ευαίσθητη είναι η είσοδος", "horizontal": "Οριζόντια", "vertical": "Κατακόρυφα", "plain": "Απλό", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Έγγραφο", + "documentStates": "Document states", "camera": "Κάμερα", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Κάτω αριστερά", "bottomRight": "Κάτω δεξιά", "zoomPosition": "Θέση ελέγχου εστίασης", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Κρύπτες", "manage": "Διαχείριση", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Input gestures", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Εγγενής γραμμή τίτλου", "mode": "Mode", "syncMode": "Λειτουργία συγχρονισμού", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Χωρίς κινητό", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Χειροκίνητα", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Αναζήτηση", "@search": { "description": "Search action" }, "properties": "Ιδιότητες", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Καρφίτσα", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Κατεύθυνση", "onlyAvailableLargerScreen": "Διαθέσιμο μόνο σε μεγαλύτερες οθόνες", "toolbarPosition": "Θέση γραμμής εργαλείων", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Περιστροφή", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Σιδηρόδρομος πλοήγησης", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Αποκοπή", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Θέμα πλατφόρμας", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Επιφάνεια Εργασίας", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "Διακομιστές ICE", "collaboration": "Συνεργασία", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Υποδοχή Ιστού", "iceServer": "Εξυπηρετητής ICE", @@ -1037,7 +1065,7 @@ "hideUI": "Απόκρυψη UI", "density": "Πυκνότητα", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Συμπαγής", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Συνεχίστε οπωσδήποτε", "zoomControl": "Έλεγχος εστίασης", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Υψηλή αντίθεση", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Αυτή η τιμή πρέπει να είναι έγκυρος αριθμός", "createAreas": "Δημιουργία περιοχών", "autosave": "Autosave", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Invert", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Μέγεθος γραμμής εργαλείων", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Προσθήκη όλων", "onlyCurrentPage": "Μόνο η τρέχουσα σελίδα", "smoothNavigation": "Ομαλή πλοήγηση", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Εναλλαγή περιοχής pan άκρης", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Χρήση Android SAF", "exact": "Ακριβής", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Γραμμές γραμμής εργαλείων", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Δοκιμή δείκτη", "pressure": "Πίεση", "small": "Μικρό", @@ -1158,6 +1207,9 @@ "selectAll": "Επιλογή όλων", "overrideTools": "Παράκαμψη εργαλείων", "hideCursorWhileDrawing": "Απόκρυψη δρομέα κατά το σχέδιο", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Εγκατεστημένο", "install": "Εγκατάσταση", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "Κατά την εκκίνηση", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Αρχική οθόνη", "lastNote": "Τελευταία σημείωση", "newNote": "Νέα σημείωση", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Παράβλεψη πίεσης", - "ignoreFirstPressureDescription": "Σε ορισμένες συσκευές, η πρώτη τιμή πίεσης δεν είναι ακριβής. Αυτή η ρύθμιση θα αγνοήσει την πρώτη τιμή πίεσης και θα χρησιμοποιήσει την πίεση του δεύτερου γεγονότος.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Προσωρινή", "simpleToolbarVisibility": "Απλή ορατότητα γραμμής εργαλείων", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Καθυστέρηση αυτόματης αποθήκευσης", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Αποθηκεύτηκε", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Φέρτε τα μετακινούμενα στοιχεία μπροστά", "addTool": "Προσθήκη εργαλείου", "nextPage": "Επόμενη σελίδα", - "previousPage": "Προηγούμενη σελίδα" + "previousPage": "Προηγούμενη σελίδα", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Τρέχουσα σελίδα", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_es.arb b/app/lib/l10n/app_es.arb index ca9b21717ca6..709aaee0974c 100644 --- a/app/lib/l10n/app_es.arb +++ b/app/lib/l10n/app_es.arb @@ -11,7 +11,13 @@ "systemTheme": "Usar tema de sistema por defecto", "view": "Ver", "contentViewport": "Vista de contenido", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Limitar la vista a coordenadas positivas", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Apagado", "canvas": "Lona", "interface": "Interfaz", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Sensibilidad", - "sensitivityHint": "Cuanto más alto sea el valor, más sensible será la entrada", "horizontal": "Horizontal", "vertical": "Vertical", "plain": "Simple", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Documento", + "documentStates": "Document states", "camera": "Cámara", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Bottom izquierdo", "bottomRight": "Botón derecho", "zoomPosition": "Posición de control de zoom", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Cachés", "manage": "Gestionar", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Input gestures", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Barra de título nativa", "mode": "Mode", "syncMode": "Modo de sincronización", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Sin móvil", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Manual", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Buscar", "@search": { "description": "Search action" }, "properties": "Propiedades", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Fijar", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Dirección", "onlyAvailableLargerScreen": "Sólo disponible en pantallas más grandes", "toolbarPosition": "Posición de barra de herramientas", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Rotar", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Raíl de navegación", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Cortar", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Tema de la plataforma", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Escritorio", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "Servidores ICE", "collaboration": "Colaboración", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Socket Web", "iceServer": "Servidor ICE", @@ -1037,7 +1065,7 @@ "hideUI": "Ocultar IU", "density": "Densidad", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Compacto", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Continuar de todos modos", "zoomControl": "Control de zoom", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Alto contraste", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Este valor debe ser un número válido", "createAreas": "Crear áreas", "autosave": "Autosave", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Invert", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Tamaño de barra de herramientas", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Añadir todo", "onlyCurrentPage": "Sólo página actual", "smoothNavigation": "Navegación suave", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Interruptor de área de sartén", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Usar SAF de Android", "exact": "Exacto", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Filas de herramienta", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Prueba de puntero", "pressure": "Presión", "small": "Pequeño", @@ -1158,6 +1207,9 @@ "selectAll": "Seleccionar todo", "overrideTools": "Anular herramientas", "hideCursorWhileDrawing": "Ocultar cursor al dibujar", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Instalado", "install": "Instalar", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "Al iniciar", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Pantalla de inicio", "lastNote": "Última nota", "newNote": "Nueva nota", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Presión de ignorancia", - "ignoreFirstPressureDescription": "En algunos dispositivos, el primer valor de presión no es preciso. Este ajuste ignorará el primer valor de presión y utilizará la presión del segundo evento en su lugar.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Temporal", "simpleToolbarVisibility": "Visibilidad simple de la barra de herramientas", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Retraso de autoguardado", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Guardado", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Trae los elementos movidos al frente", "addTool": "Añadir herramienta", "nextPage": "Página siguiente", - "previousPage": "Página anterior" + "previousPage": "Página anterior", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Página actual", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_fi.arb b/app/lib/l10n/app_fi.arb index 57af219c4811..50aa2d38150c 100644 --- a/app/lib/l10n/app_fi.arb +++ b/app/lib/l10n/app_fi.arb @@ -11,7 +11,13 @@ "systemTheme": "Käytä järjestelmän oletusteemaa", "view": "Näytä", "contentViewport": "Sisällön Näyttöportti", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Rajoita näkymä positiivisiin koordinaatteihin", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Pois", "canvas": "Kanava", "interface": "Käyttöliittymä", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Herkkyys", - "sensitivityHint": "Mitä suurempi arvo on, sitä herkempiä syöte", "horizontal": "Vaakasuora", "vertical": "Pystysuora", "plain": "Tavallinen", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Asiakirja", + "documentStates": "Document states", "camera": "Kamera", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Vasen alareuna", "bottomRight": "Oikea alareuna", "zoomPosition": "Zoomauksen ohjauksen sijainti", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Kätköt", "manage": "Hallitse", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Input gestures", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Alkuperäinen otsikkopalkki", "mode": "Mode", "syncMode": "Synkronoinnin tila", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Ei matkapuhelinta", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Manuaalinen", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Etsi", "@search": { "description": "Search action" }, "properties": "Ominaisuudet", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Kiinnitä", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Suunta", "onlyAvailableLargerScreen": "Saatavilla vain suuremmilla näytöillä", "toolbarPosition": "Työkalupalkin sijainti", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Kierrä", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Navigointi rautatie", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Leikkaa", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Alustan teema", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Työpöytä", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "Ice- Palvelimet", "collaboration": "Yhteistyö", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Web- Socket", "iceServer": "Ice- Palvelin", @@ -1037,7 +1065,7 @@ "hideUI": "Piilota Käyttöliittymä", "density": "Tiheys", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Kompakti", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Jatka joka tapauksessa", "zoomControl": "Zoomauksen hallinta", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Korkea kontrasti", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Tämän arvon on oltava kelvollinen numero", "createAreas": "Luo alueita", "autosave": "Autosave", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Invert", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Työkalupalkin koko", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Lisää kaikki", "onlyCurrentPage": "Vain nykyinen sivu", "smoothNavigation": "Pehmeä navigointi", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Alueen reuna-alueen vaihtaminen", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Käytä Android SAF", "exact": "Tarkka", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Työkalupalkin rivit", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Osoittimen testi", "pressure": "Paine", "small": "Pieni", @@ -1158,6 +1207,9 @@ "selectAll": "Valitse kaikki", "overrideTools": "Ohita työkalut", "hideCursorWhileDrawing": "Piilota kohdistin piirroksen aikana", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Asennettu", "install": "Asenna", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "Käynnistyksessä", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Kotinäyttö", "lastNote": "Viimeisin merkintä", "newNote": "Uusi muistiinpano", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ohita paine", - "ignoreFirstPressureDescription": "Joissain laitteissa ensimmäinen paineen arvo ei ole tarkka. Tämä asetus jättää huomiotta ensimmäisen paineen arvon ja käyttää sen sijaan toisen tapahtuman painetta.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Väliaikainen", "simpleToolbarVisibility": "Yksinkertainen työkalupalkin näkyvyys", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Automaattisen tallennuksen viive", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Tallennettu", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Tuo siirretyt elementit eteen", "addTool": "Lisää työkalu", "nextPage": "Seuraava sivu", - "previousPage": "Edellinen sivu" + "previousPage": "Edellinen sivu", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Nykyinen sivu", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_fr.arb b/app/lib/l10n/app_fr.arb index 1c5be5593545..d2a22e4b5b90 100644 --- a/app/lib/l10n/app_fr.arb +++ b/app/lib/l10n/app_fr.arb @@ -11,7 +11,13 @@ "systemTheme": "Utiliser le thème système par défaut", "view": "Voir", "contentViewport": "Vue du contenu", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Limiter le Viewport aux coordonnées positives", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Désactivé", "canvas": "Toile", "interface": "Interface", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Sensibilité", - "sensitivityHint": "Plus la valeur est élevée, plus l'entrée est sensible", "horizontal": "Horizontal", "vertical": "Vertical", "plain": "Plaine", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Document", + "documentStates": "Document states", "camera": "Appareil photo", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "En bas à gauche", "bottomRight": "En bas à droite", "zoomPosition": "Position de contrôle du zoom", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Caches", "manage": "Gérer", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Input gestures", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Barre de titre native", "mode": "Mode", "syncMode": "Mode de synchronisation", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Aucun mobile", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Manuelle", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Chercher", "@search": { "description": "Search action" }, "properties": "Propriétés", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Épingler", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Orientation", "onlyAvailableLargerScreen": "Disponible uniquement sur les écrans plus grands", "toolbarPosition": "Position de la barre d'outils", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Faire pivoter", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Rail de navigation", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Couper", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Thème de la plateforme", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Bureau", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "Serveur ICE", "collaboration": "Collaboration", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Web Socket", "iceServer": "Serveur ICE", @@ -1037,7 +1065,7 @@ "hideUI": "Cacher l'interface utilisateur", "density": "Densité", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Compact", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Continuer quand même", "zoomControl": "Contrôle du zoom", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Contraste élevé", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Cette valeur doit être un nombre valide", "createAreas": "Créer des zones", "autosave": "Autosave", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Invert", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Taille de la barre d'outils", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Ajouter tout", "onlyCurrentPage": "Uniquement la page courante", "smoothNavigation": "Navigation fluide", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Changement de zone de la bordure", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Utiliser Android SAF", "exact": "Exactement", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Lignes de barre d'outils", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Test du pointeur", "pressure": "Pression", "small": "Petit", @@ -1158,6 +1207,9 @@ "selectAll": "Tout sélectionner", "overrideTools": "Remplacer les outils", "hideCursorWhileDrawing": "Masquer le curseur pendant le dessin", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Installé", "install": "Installer", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "Au démarrage", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Écran d'accueil", "lastNote": "Dernière note", "newNote": "Nouvelle note", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignorer la pression", - "ignoreFirstPressureDescription": "Sur certains appareils, la première valeur de pression n'est pas précise. Ce paramètre ignorera la première valeur de pression et utilisera la pression du deuxième événement à la place.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Temporaire", "simpleToolbarVisibility": "Visibilité simple de la barre d'outils", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Délai d'enregistrement automatique", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Enregistré", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Apporter les éléments déplacés à l'avant", "addTool": "Ajouter un outil", "nextPage": "Page suivante", - "previousPage": "Page précédente" + "previousPage": "Page précédente", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Page actuelle", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_he.arb b/app/lib/l10n/app_he.arb index cbf245ffaf22..3b1013b0bfc1 100644 --- a/app/lib/l10n/app_he.arb +++ b/app/lib/l10n/app_he.arb @@ -11,7 +11,13 @@ "systemTheme": "השתמש בערכת הנושא של המערכת", "view": "תצוגה", "contentViewport": "Content Viewport", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Limit Viewport to positive coordinates", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Off", "canvas": "Canvas", "interface": "Interface", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "רגישות", - "sensitivityHint": "ככל שהערך גבוה יותר, כך הקלט רגיש יותר", "horizontal": "אופקי", "vertical": "אנכי", "plain": "חלק", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "מסמך", + "documentStates": "Document states", "camera": "מצלמה", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "שמאל למטה", "bottomRight": "ימין למטה", "zoomPosition": "Zoom control position", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "מטמונים", "manage": "נהל", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "מחוות קלט", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "סרגל כותרת מקורי", "mode": "Mode", "syncMode": "מצב סנכרון", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "לא בנייד", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "ידני", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "חיפוש", "@search": { "description": "Search action" }, "properties": "מאפיינים", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "נעץ", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Direction", "onlyAvailableLargerScreen": "זמין רק במסכים גדולים יותר", "toolbarPosition": "מיקום סרגל כלים", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "סובב", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "פס ניווט", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "גזור", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "ערכת נושא של הפלטפורמה", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "שולחן עבודה", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "שרתי ICE", "collaboration": "שיתוף פעולה", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Web Socket", "iceServer": "שרת ICE", @@ -1037,7 +1065,7 @@ "hideUI": "הסתר ממשק משתמש", "density": "צפיפות", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "קומפקטי", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "המשך בכל זאת", "zoomControl": "בקרת זום", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "ניגודיות גבוהה", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "ערך זה חייב להיות מספר תקין", "createAreas": "צור אזורים", "autosave": "שמירה אוטומטית", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "הפוך", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "גודל סרגל כלים", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "הוסף הכל", "onlyCurrentPage": "רק העמוד הנוכחי", "smoothNavigation": "ניווט חלק", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Edge pan area switching", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Use Android SAF", "exact": "מדויק", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "שורות בסרגל הכלים", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "בדיקת מצביע", "pressure": "לחץ", "small": "קטן", @@ -1158,6 +1207,9 @@ "selectAll": "בחר הכל", "overrideTools": "דרוס כלים", "hideCursorWhileDrawing": "הסתר סמן בזמן ציור", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "מותקן", "install": "התקן", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "בעת הפעלה", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "מסך הבית", "lastNote": "פתק אחרון", "newNote": "פתק חדש", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignore pressure", - "ignoreFirstPressureDescription": "On some devices, the first pressure value is not accurate. This setting will ignore the first pressure value and use the pressure of the second event instead.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Temporary", "simpleToolbarVisibility": "Simple toolbar visibility", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Autosave delay", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Saved", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Bring moved elements to front", "addTool": "Add tool", "nextPage": "Next page", - "previousPage": "Previous page" + "previousPage": "Previous page", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Current page", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_hi.arb b/app/lib/l10n/app_hi.arb index aef62f74eb32..0df963e4a189 100644 --- a/app/lib/l10n/app_hi.arb +++ b/app/lib/l10n/app_hi.arb @@ -11,7 +11,13 @@ "systemTheme": "सिस्टम की डिफ़ॉल्ट थीम का उपयोग करें", "view": "देखें", "contentViewport": "Content Viewport", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Limit Viewport to positive coordinates", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Off", "canvas": "Canvas", "interface": "Interface", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "संवेदनशीलता", - "sensitivityHint": "मान जितना अधिक होगा, इनपुट उतना ही संवेदनशील होगा", "horizontal": "क्षैतिज", "vertical": "लंबवत", "plain": "सादा", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "दस्तावेज़", + "documentStates": "Document states", "camera": "कैमरा", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "नीचे बाएँ", "bottomRight": "नीचे दाएँ", "zoomPosition": "Zoom control position", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "कैश", "manage": "प्रबंधित करें", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "इनपुट जेस्चर", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "नेटिव टाइटल बार", "mode": "Mode", "syncMode": "सिंक मोड", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "कोई मोबाइल नहीं", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "मैनुअल", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "खोजें", "@search": { "description": "Search action" }, "properties": "गुण", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "पिन करें", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Direction", "onlyAvailableLargerScreen": "केवल बड़ी स्क्रीन पर उपलब्ध है", "toolbarPosition": "टूलबार की स्थिति", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "घुमाएं", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "नेविगेशन रेल", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "काटें", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "प्लेटफ़ॉर्म थीम", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "डेस्कटॉप", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "ICE सर्वर्स", "collaboration": "सहयोग", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Web Socket", "iceServer": "ICE सर्वर", @@ -1037,7 +1065,7 @@ "hideUI": "UI छिपाएं", "density": "घनत्व", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "कॉम्पैक्ट", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "फिर भी जारी रखें", "zoomControl": "ज़ूम नियंत्रण", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "उच्च कंट्रास्ट", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "यह मान एक मान्य संख्या होनी चाहिए", "createAreas": "क्षेत्र बनाएं", "autosave": "ऑटोसेव", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "उलट दें", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "टूलबार का आकार", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "सभी जोड़ें", "onlyCurrentPage": "केवल वर्तमान पेज", "smoothNavigation": "सहज नेविगेशन", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Edge pan area switching", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Use Android SAF", "exact": "सटीक", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "टूलबार पंक्तियाँ", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "पॉइंटर टेस्ट", "pressure": "दबाव", "small": "छोटा", @@ -1158,6 +1207,9 @@ "selectAll": "सभी चुनें", "overrideTools": "टूल्स ओवरराइड करें", "hideCursorWhileDrawing": "ड्राइंग करते समय कर्सर छिपाएं", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "स्थापित", "install": "इंस्टॉल करें", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "स्टार्टअप पर", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "होम स्क्रीन", "lastNote": "अंतिम नोट", "newNote": "नया नोट", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignore pressure", - "ignoreFirstPressureDescription": "On some devices, the first pressure value is not accurate. This setting will ignore the first pressure value and use the pressure of the second event instead.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Temporary", "simpleToolbarVisibility": "Simple toolbar visibility", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Autosave delay", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Saved", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Bring moved elements to front", "addTool": "Add tool", "nextPage": "Next page", - "previousPage": "Previous page" + "previousPage": "Previous page", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Current page", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_hu.arb b/app/lib/l10n/app_hu.arb index b3bb4b8593aa..5dd3199f1361 100644 --- a/app/lib/l10n/app_hu.arb +++ b/app/lib/l10n/app_hu.arb @@ -11,7 +11,13 @@ "systemTheme": "Rendszer téma", "view": "Megtekintés", "contentViewport": "Content Viewport", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Limit Viewport to positive coordinates", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Off", "canvas": "Canvas", "interface": "Interface", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Érzékenység", - "sensitivityHint": "Minél nagyobb az érték, annál érzékenyebb a bemenet", "horizontal": "Vízszintes", "vertical": "Függőleges", "plain": "Sima", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Dokumentum", + "documentStates": "Document states", "camera": "Kamera", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Bal alsó", "bottomRight": "Jobb alsó", "zoomPosition": "Zoom control position", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Gyorsítótárak", "manage": "Kezelés", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Beviteli gesztusok", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Natív címsor", "mode": "Mode", "syncMode": "Szinkronizációs mód", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Mobilhálózat letiltva", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Kézi", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Keresés", "@search": { "description": "Search action" }, "properties": "Tulajdonságok", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Rögzítés", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Direction", "onlyAvailableLargerScreen": "Csak nagyobb képernyőkön érhető el", "toolbarPosition": "Eszköztár pozíció", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Elforgatás", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Navigációs sáv", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Kivágás", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Platform téma", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Asztali", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "ICE szerverek", "collaboration": "Együttműködés", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "WebRTC", "webSocket": "WebSocket", "iceServer": "ICE szerver", @@ -1037,7 +1065,7 @@ "hideUI": "Felület elrejtése", "density": "Sűrűség", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Tömör", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Mindenképpen folytat", "zoomControl": "Nagyítás vezérlése", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Magas kontraszt", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Ennek az értéknek érvényes számnak kell lennie", "createAreas": "Területek létrehozása", "autosave": "Automatikus mentés", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Invertálás", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Eszköztár méret", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Összes hozzáadása", "onlyCurrentPage": "Csak aktuális oldal", "smoothNavigation": "Sima navigáció", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Edge pan area switching", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Use Android SAF", "exact": "Pontos", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Eszköztár sorok", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Mutató teszt", "pressure": "Nyomás", "small": "Kicsi", @@ -1158,6 +1207,9 @@ "selectAll": "Mindet kijelöl", "overrideTools": "Eszközök felülírása", "hideCursorWhileDrawing": "Rajzolás közben a kurzor elrejtése", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Telepítve", "install": "Telepítés", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "Indításkor", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Kezdőképernyő", "lastNote": "Utolsó jegyzet", "newNote": "Új jegyzet", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignore pressure", - "ignoreFirstPressureDescription": "On some devices, the first pressure value is not accurate. This setting will ignore the first pressure value and use the pressure of the second event instead.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Temporary", "simpleToolbarVisibility": "Simple toolbar visibility", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Autosave delay", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Saved", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Bring moved elements to front", "addTool": "Add tool", "nextPage": "Next page", - "previousPage": "Previous page" + "previousPage": "Previous page", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Current page", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_id.arb b/app/lib/l10n/app_id.arb index cd58a7ca3a61..9024eb725aae 100644 --- a/app/lib/l10n/app_id.arb +++ b/app/lib/l10n/app_id.arb @@ -11,7 +11,13 @@ "systemTheme": "Gunakan tema sistem bawaan", "view": "Tampilan", "contentViewport": "Area pandang konten", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Batasi area pandang ke koordinat positif", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Mati", "canvas": "Kanvas", "interface": "Antarmuka", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Sensitivitas", - "sensitivityHint": "Semakin tinggi nilainya, semakin sensitif masukannya", "horizontal": "Horizontal", "vertical": "Vertikal", "plain": "Polos", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Dokumen", + "documentStates": "Document states", "camera": "Kamera", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Kiri bawah", "bottomRight": "Kanan bawah", "zoomPosition": "Posisi kontrol zoom", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Cache", "manage": "Kelola", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Gestur masukan", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Bilah judul asli", "mode": "Mode", "syncMode": "Mode sinkronisasi", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Tanpa seluler", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Manual", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Cari", "@search": { "description": "Search action" }, "properties": "Properti", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Sematkan", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Direction", "onlyAvailableLargerScreen": "Hanya tersedia di layar yang lebih besar", "toolbarPosition": "Posisi bilah alat", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Putar", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Rel navigasi", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Potong", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Tema platform", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Desktop", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "Server ICE", "collaboration": "Kolaborasi", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "WebRTC", "webSocket": "WebSocket", "iceServer": "Server ICE", @@ -1037,7 +1065,7 @@ "hideUI": "Sembunyikan UI", "density": "Kepadatan", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Ringkas", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Tetap lanjutkan", "zoomControl": "Kontrol zoom", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Tampilkan thumbnail", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Kontras tinggi", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Nilai ini harus berupa angka yang valid", "createAreas": "Buat area", "autosave": "Simpan otomatis", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Balik", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Ukuran bilah alat", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Tambahkan semua", "onlyCurrentPage": "Hanya halaman saat ini", "smoothNavigation": "Navigasi halus", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Pergantian area dengan pan tepi", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Gunakan Android SAF", "exact": "Tepat", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Baris bilah alat", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Tes penunjuk", "pressure": "Tekanan", "small": "Kecil", @@ -1158,6 +1207,9 @@ "selectAll": "Pilih semua", "overrideTools": "Timpa alat", "hideCursorWhileDrawing": "Sembunyikan kursor saat menggambar", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Terpasang", "install": "Pasang", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "Saat mulai", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Layar beranda", "lastNote": "Catatan terakhir", "newNote": "Catatan baru", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Abaikan tekanan", - "ignoreFirstPressureDescription": "Pada beberapa perangkat, nilai tekanan pertama tidak akurat. Pengaturan ini akan mengabaikan nilai tekanan pertama dan menggunakan tekanan dari peristiwa kedua.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Sementara", "simpleToolbarVisibility": "Visibilitas bilah alat sederhana", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Penundaan simpan otomatis", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Tersimpan", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Bring moved elements to front", "addTool": "Add tool", "nextPage": "Next page", - "previousPage": "Previous page" + "previousPage": "Previous page", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Current page", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_it.arb b/app/lib/l10n/app_it.arb index a22d9ed2a74a..2294f8b90011 100644 --- a/app/lib/l10n/app_it.arb +++ b/app/lib/l10n/app_it.arb @@ -11,7 +11,13 @@ "systemTheme": "Usa tema di sistema predefinito", "view": "Visualizza", "contentViewport": "Viewport Contenuto", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Limita la visualizzazione alle coordinate positive", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Off", "canvas": "Tela", "interface": "Interfaccia", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Sensibilità", - "sensitivityHint": "Più alto è il valore, più sensibile è l'input", "horizontal": "Orizzontale", "vertical": "Verticale", "plain": "Semplice", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Documento", + "documentStates": "Document states", "camera": "Fotocamera", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Basso a sinistra", "bottomRight": "Basso a destra", "zoomPosition": "Posizione controllo zoom", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Cache", "manage": "Gestisci", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Gestures di input", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Barra del titolo nativo", "mode": "Mode", "syncMode": "Modalità sincronizzazione", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Nessun cellulare", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Manuale", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Cerca", "@search": { "description": "Search action" }, "properties": "Proprietà", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Pin", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Direzione", "onlyAvailableLargerScreen": "Disponibile solo su schermi più grandi", "toolbarPosition": "Posizione barra strumenti", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Ruota", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Barra di navigazione", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Taglia", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Tema piattaforma", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Desktop", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "Server ICE", "collaboration": "Collaborazione", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Web Socket", "iceServer": "Server ICE", @@ -1037,7 +1065,7 @@ "hideUI": "Nascondi UI", "density": "Densità", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Compatto", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Continua comunque", "zoomControl": "Controllo zoom", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Alto contrasto", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Questo valore deve essere valido", "createAreas": "Crea Area", "autosave": "Salvataggio automatico", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Invert", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Dimensione barra strumenti", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Aggiungi tutto", "onlyCurrentPage": "Solo la pagina corrente", "smoothNavigation": "Navigazione fluida", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Interruttore dell'area del bordo", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Usa Android SAF", "exact": "Esatto", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Righe barra strumenti", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Test puntatore", "pressure": "Pressione", "small": "Piccolo", @@ -1158,6 +1207,9 @@ "selectAll": "Seleziona tutto", "overrideTools": "Sovrascrivi strumenti", "hideCursorWhileDrawing": "Nascondi il cursore durante il disegno", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Installato", "install": "Installa", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "All'avvio", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Schermata home", "lastNote": "Ultima nota", "newNote": "Nuova nota", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignora pressione", - "ignoreFirstPressureDescription": "Su alcuni dispositivi, il primo valore di pressione non è accurato. Questa impostazione ignorerà il primo valore di pressione e userà invece la pressione del secondo evento.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Temporaneo", "simpleToolbarVisibility": "Semplice visibilità della barra degli strumenti", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Ritardo salvataggio automatico", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Salvato", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Porta in primo piano gli elementi spostati", "addTool": "Aggiungi strumento", "nextPage": "Pagina successiva", - "previousPage": "Pagina precedente" + "previousPage": "Pagina precedente", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Pagina corrente", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_ja.arb b/app/lib/l10n/app_ja.arb index 4049fce44432..408e1003f9d8 100644 --- a/app/lib/l10n/app_ja.arb +++ b/app/lib/l10n/app_ja.arb @@ -11,7 +11,13 @@ "systemTheme": "デフォルトのシステムテーマを使用する", "view": "表示", "contentViewport": "コンテンツビュー", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "ビューポートを正座標に制限する", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "オフ", "canvas": "Canvas", "interface": "インターフェイス", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "感度", - "sensitivityHint": "値が高いほど入力の感度が高くなります", "horizontal": "水平方向", "vertical": "垂直方向", "plain": "Plain", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "ドキュメント", + "documentStates": "Document states", "camera": "カメラ", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "左下", "bottomRight": "右下", "zoomPosition": "ズーム制御位置", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "キャッシュ", "manage": "管理", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Input gestures", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "ネイティブのタイトルバー", "mode": "Mode", "syncMode": "同期モード", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "携帯電話がありません", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "マニュアル", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "検索", "@search": { "description": "Search action" }, "properties": "プロパティー", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "ピン留めする", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "方向", "onlyAvailableLargerScreen": "より大きな画面でのみ利用可能", "toolbarPosition": "ツールバーの位置", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "回転", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "ナビゲーション レール", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "切り取り", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "プラットフォームのテーマ", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "デスクトップ", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "ICE サーバー", "collaboration": "コラボレーション", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Web Socket", "iceServer": "ICE サーバー", @@ -1037,7 +1065,7 @@ "hideUI": "UI を隠す", "density": "解像度:", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "コンパクト化", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "それでも続ける", "zoomControl": "ズームコントロール", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "高コントラストformat@@0", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "この値は有効な数字でなければなりません", "createAreas": "エリアを作成", "autosave": "Autosave", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Invert", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "ツールバーのサイズ", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "すべて追加", "onlyCurrentPage": "現在のページのみ", "smoothNavigation": "スムーズなナビゲーション", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "エッジパンエリアの切り替え", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Android SAFを使用する", "exact": "正確な", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "ツールバーの行", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "ポインタテスト", "pressure": "", "small": "小", @@ -1158,6 +1207,9 @@ "selectAll": "すべて選択", "overrideTools": "ツールを上書きする", "hideCursorWhileDrawing": "描画中にカーソルを隠す", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "インストール済み", "install": "インストール", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "起動時", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "ホーム画面", "lastNote": "最後のメモ", "newNote": "新規ノート", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "プレッシャーを無視", - "ignoreFirstPressureDescription": "デバイスによっては、最初の圧力値が正確でない場合があります。 この設定は、最初のプレッシャー値を無視し、代わりに2番目のイベントのプレッシャーを使用します。", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "一時的な", "simpleToolbarVisibility": "シンプルなツールバーの表示", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "自動保存の遅延", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "保存しました", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "移動した要素を前面に移動", "addTool": "ツールを追加", "nextPage": "次のページ", - "previousPage": "前のページ" + "previousPage": "前のページ", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "現在のページ", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_ko.arb b/app/lib/l10n/app_ko.arb index c8fc51ef035b..657de7c798fa 100644 --- a/app/lib/l10n/app_ko.arb +++ b/app/lib/l10n/app_ko.arb @@ -11,7 +11,13 @@ "systemTheme": "시스템 기본 테마 사용", "view": "보기", "contentViewport": "Content Viewport", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Limit Viewport to positive coordinates", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Off", "canvas": "Canvas", "interface": "Interface", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "민감도", - "sensitivityHint": "값이 높을수록 입력이 더 민감해집니다", "horizontal": "가로", "vertical": "세로", "plain": "무지", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "문서", + "documentStates": "Document states", "camera": "카메라", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "왼쪽 아래", "bottomRight": "오른쪽 아래", "zoomPosition": "Zoom control position", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "캐시", "manage": "관리", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "입력 제스처", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "네이티브 제목 표시줄", "mode": "Mode", "syncMode": "동기화 모드", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "모바일 제외", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "수동", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "검색", "@search": { "description": "Search action" }, "properties": "속성", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "고정", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Direction", "onlyAvailableLargerScreen": "더 큰 화면에서만 사용 가능", "toolbarPosition": "도구 모음 위치", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "회전", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "탐색 레일", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "잘라내기", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "플랫폼 테마", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "데스크톱", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "ICE 서버", "collaboration": "협업", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "웹 소켓", "iceServer": "ICE 서버", @@ -1037,7 +1065,7 @@ "hideUI": "UI 숨기기", "density": "밀도", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "좁게", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "계속하기", "zoomControl": "확대/축소 제어", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "고대비", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "이 값은 유효한 숫자여야 합니다", "createAreas": "영역 만들기", "autosave": "자동 저장", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "반전", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "도구 모음 크기", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "모두 추가", "onlyCurrentPage": "현재 페이지만", "smoothNavigation": "부드러운 탐색", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Edge pan area switching", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Use Android SAF", "exact": "정확", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "도구 모음 행", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "포인터 테스트", "pressure": "필압", "small": "작음", @@ -1158,6 +1207,9 @@ "selectAll": "모두 선택", "overrideTools": "도구 재정의", "hideCursorWhileDrawing": "그리는 동안 커서 숨기기", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "설치됨", "install": "설치", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "시작 시", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "홈 화면", "lastNote": "마지막 노트", "newNote": "새 노트", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignore pressure", - "ignoreFirstPressureDescription": "On some devices, the first pressure value is not accurate. This setting will ignore the first pressure value and use the pressure of the second event instead.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Temporary", "simpleToolbarVisibility": "Simple toolbar visibility", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Autosave delay", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Saved", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Bring moved elements to front", "addTool": "Add tool", "nextPage": "Next page", - "previousPage": "Previous page" + "previousPage": "Previous page", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Current page", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_nl.arb b/app/lib/l10n/app_nl.arb index 33ddaaffaa95..25c71c2bc19f 100644 --- a/app/lib/l10n/app_nl.arb +++ b/app/lib/l10n/app_nl.arb @@ -11,7 +11,13 @@ "systemTheme": "Gebruik standaard systeemthema", "view": "Bekijken", "contentViewport": "Weergave inhoud", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Beperk Viewport tot positieve coördinaten", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "UIT", "canvas": "Canvas", "interface": "Interface", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Gevoeligheid", - "sensitivityHint": "Hoe hoger de waarde, hoe gevoeliger de input", "horizontal": "Horizontaal", "vertical": "Verticaal", "plain": "Eenvoudig", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Document", + "documentStates": "Document states", "camera": "camera", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Links onder", "bottomRight": "Onder rechts", "zoomPosition": "Zoom bedieningspositie", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Caches", "manage": "Beheren", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Input gestures", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Oorspronkelijke titelbalk", "mode": "Mode", "syncMode": "Synchronisatie modus", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Geen mobiel", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Handleiding", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Zoeken", "@search": { "description": "Search action" }, "properties": "Eigenschappen", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Vastzetten", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Richting", "onlyAvailableLargerScreen": "Alleen beschikbaar op grotere schermen", "toolbarPosition": "Werkbalk positie", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Draaien", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Navigatie spoor", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Knippen", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Platform thema", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Startscherm", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "ICE servers", "collaboration": "Samenwerken", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Web Socket", "iceServer": "ICE server", @@ -1037,7 +1065,7 @@ "hideUI": "Verberg UI", "density": "Dichtheid", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Compacte", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Toch doorgaan", "zoomControl": "Zoom beheer", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Hoog contrast", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Deze waarde moet een geldig nummer zijn", "createAreas": "Maak gebieden", "autosave": "Autosave", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Invert", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Werkbalk grootte", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Alles toevoegen", "onlyCurrentPage": "Alleen huidige pagina", "smoothNavigation": "Vloeiende navigatie", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Edge pan gebied wisselen", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Gebruik Android SAF", "exact": "Exacte", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Werkbalk rijen", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Testen van aanwijzer", "pressure": "Drukdruk", "small": "Klein", @@ -1158,6 +1207,9 @@ "selectAll": "Alles selecteren", "overrideTools": "Hulpmiddelen vervangen", "hideCursorWhileDrawing": "Verberg cursor tijdens tekenen", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Geinstalleerd", "install": "Installeren", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "Bij het opstarten", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Startscherm", "lastNote": "Laatste opmerking", "newNote": "Nieuwe notitie", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Negeer druk", - "ignoreFirstPressureDescription": "Op sommige apparaten is de eerste drukwaarde niet nauwkeurig. Deze instelling negeert de eerste drukwaarde en gebruikt in plaats daarvan de druk van de tweede gebeurtenis.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Tijdelijk", "simpleToolbarVisibility": "Eenvoudige werkbalk zichtbaarheid", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Automatisch opslaan vertraging", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Opgeslagen", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Breng verplaatste elementen naar voren", "addTool": "Functie toevoegen", "nextPage": "Volgende pagina", - "previousPage": "Vorige pagina" + "previousPage": "Vorige pagina", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Huidige pagina", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_no.arb b/app/lib/l10n/app_no.arb index b521d60d89f7..9c8b3cdb841c 100644 --- a/app/lib/l10n/app_no.arb +++ b/app/lib/l10n/app_no.arb @@ -11,7 +11,13 @@ "systemTheme": "Bruk standard systemtema", "view": "Vis", "contentViewport": "Innholdsvisnings port", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Begrens visningsporten til positive koordinater", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Av", "canvas": "Canvas", "interface": "Grensesnitt", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Sensitivitet", - "sensitivityHint": "Jo høyere verdien er, desto mer følsom inndata", "horizontal": "Horisontal", "vertical": "Vertikal", "plain": "Enkel", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Dokument", + "documentStates": "Document states", "camera": "Kamera", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Nederst til venstre", "bottomRight": "Nederst til høyre", "zoomPosition": "Zoom kontrollposisjon", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Cacher", "manage": "Administrer", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Input gestures", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Native tittellinjen", "mode": "Mode", "syncMode": "Synk modus", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Ingen mobil", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Manuell", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Søk", "@search": { "description": "Search action" }, "properties": "Egenskaper", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Fest", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Retning", "onlyAvailableLargerScreen": "Bare tilgjengelig på større skjermer", "toolbarPosition": "Plassering av verktøylinje", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Roter", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Navigasjon bane", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Klipp", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Plattform-tema", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Skrivebord", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "ICE servere", "collaboration": "Samarbeid", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Web Socket", "iceServer": "ICE server", @@ -1037,7 +1065,7 @@ "hideUI": "Skjul brukergrensesnitt", "density": "Tetthet", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Kompakt", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Fortsett likevel", "zoomControl": "Zoom kontroll", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Høy kontrast", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Denne verdien må være et gyldig tall", "createAreas": "Opprett områder", "autosave": "Autosave", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Invert", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Størrelse på verktøylinje", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Legg til alle", "onlyCurrentPage": "Bare gjeldende side", "smoothNavigation": "Jevn navigasjon", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Pan område område av kant", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Bruk Android SAF", "exact": "Nøyaktig", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Linjer på verktøylinje", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Pekers test", "pressure": "Trykk", "small": "Liten", @@ -1158,6 +1207,9 @@ "selectAll": "Velg alle", "overrideTools": "Overstyr verktøy", "hideCursorWhileDrawing": "Skjul markøren under tegning", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Installert", "install": "Installer", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "Ved oppstart", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Hjem-skjerm", "lastNote": "Siste notat", "newNote": "Nytt notat", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignorer trykk", - "ignoreFirstPressureDescription": "På noen enheter er den første trykkverdien ikke nøyaktig. Denne innstillingen vil ignorere den første trykkverdien, og bruke trykket til den andre hendelsen, i stedet.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Midlertidig", "simpleToolbarVisibility": "Enkel verktøylinjens synlighet", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Autolagring forsinkelse", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Lagret", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Plasser flyttede elementer forsiden", "addTool": "Legg til verktøy", "nextPage": "Neste side", - "previousPage": "Forrige side" + "previousPage": "Forrige side", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Nåværende side", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_or.arb b/app/lib/l10n/app_or.arb index 3e7dc62aff79..4eb1451b4aa9 100644 --- a/app/lib/l10n/app_or.arb +++ b/app/lib/l10n/app_or.arb @@ -11,7 +11,13 @@ "systemTheme": "ଡିଫଲ୍ଟ ସିଷ୍ଟମ୍ ଥୀମ୍ ବ୍ୟବହାର କରନ୍ତୁ", "view": "ଦେଖନ୍ତୁ", "contentViewport": "Content Viewport", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Limit Viewport to positive coordinates", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Off", "canvas": "Canvas", "interface": "Interface", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "ସଂවේଦନଶୀଳତା", - "sensitivityHint": "ଅଧିକ ମୂଲ୍ୟ, ଅଧିକ ସଂਵੇଦନଶୀଳ", "horizontal": "ଆଡ଼", "vertical": "ଉର୍ଧ୍ୱା", "plain": "ସାଧାରଣ", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "ଡକ୍ୟୁମେଣ୍ଟ", + "documentStates": "Document states", "camera": "କ୍ୟାମେରା", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "ତଳ ବାମ", "bottomRight": "ତଳ ଦକ୍ଷିଣ", "zoomPosition": "Zoom control position", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "କ୍ୟାଶଗୁଡିକ", "manage": "ପରିଚାଳନା", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "ଇନ୍ପୁଟ୍ ଇଶାରାଗୁଡିକ", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "ନେଟିଭ୍ ଟାଇଟେଲ୍ ବାର୍", "mode": "Mode", "syncMode": "ସିଙ୍କ୍ ମୋଡ୍", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "ମୋବାଇଲ୍ ନୁହେଁ", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "ମାନୁଆଲ୍", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "ଶୋଧନ", "@search": { "description": "Search action" }, "properties": "ଗୁଣ", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "ପିନ୍", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Direction", "onlyAvailableLargerScreen": "ମାତ୍ର ବଡ଼ ସ୍କ୍ରିନ୍ ଉପଲବ୍ଧ", "toolbarPosition": "ଟୁଲ୍ବାର ସ୍ଥାନ", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "ଘୁରାନ୍ତୁ", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "ନାଭିଗେସନ୍ ରେଲ୍", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "କଟ୍", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "ଛୋଟ", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "ଡେସ୍କଟପ୍", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "ICE ସର୍ବର୍ଗୁଡିକ", "collaboration": "ସହଯୋଗ", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Web Socket", "iceServer": "ICE ସର୍ବର୍", @@ -1037,7 +1065,7 @@ "hideUI": "UI ଲୁଚାନ୍ତୁ", "density": "ଘନତା", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "ସଂକ୍ଷିପ୍ତ", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "ତଥାପି ଆଗକୁ ବଢ଼ନ୍ତୁ", "zoomControl": "ଜୁମ୍ ନିୟନ୍ତ୍ରଣ", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "ଉଚ୍ଚ ବିପରୀତତା", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "ଏହି ମୂଲ୍ୟଟି ବୈଧ ସଂଖ୍ୟା ହେବା ଉଚିତ", "createAreas": "ଅଞ୍ଚଳ ତିଆରି କରନ୍ତୁ", "autosave": "ସ୍ଵୟଂଚାଳିତ ସଞ୍ଚୟ", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "ବିପରୀତ", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "ଟୁଲ୍ବାର ଆକାର", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "ସବୁଯୋଗ କରନ୍ତୁ", "onlyCurrentPage": "କେବଳ ଚଳୁ ପୃଷ୍ଠା", "smoothNavigation": "ସ୍ମୂଥ୍ ନାଭିଗେସନ୍", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Edge pan area switching", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Use Android SAF", "exact": "ଖୋଲ୍", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "ଟୁଲ୍ବାର ତାଲିକା", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "ପଏଣ୍ଟର୍ ପରୀକ୍ଷା", "pressure": "ଦବାଇ", "small": "ଛୋଟ", @@ -1158,6 +1207,9 @@ "selectAll": "ସବୁ ଚୟନ କରନ୍ତୁ", "overrideTools": "ଟୁଲ୍ ଓଭର୍ରାଇଟ୍", "hideCursorWhileDrawing": "ଚିତ୍ରାଙ୍କନ କାଳୀନ କର୍ସର୍ ଲୁଚାନ୍ତୁ", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "ଇନ୍‌ସ୍ଟଲ୍ ହେଲା", "install": "ଇନ୍‌ସ୍ଟଲ୍", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "ଆରମ୍ଭରେ", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "ହୋମ୍ ସ୍କ୍ରିନ୍", "lastNote": "ଶେଷ ଟିପ୍ପଣୀ", "newNote": "ନୂତନ ଟିପ୍ପଣୀ", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignore pressure", - "ignoreFirstPressureDescription": "On some devices, the first pressure value is not accurate. This setting will ignore the first pressure value and use the pressure of the second event instead.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Temporary", "simpleToolbarVisibility": "Simple toolbar visibility", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Autosave delay", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Saved", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Bring moved elements to front", "addTool": "Add tool", "nextPage": "Next page", - "previousPage": "Previous page" + "previousPage": "Previous page", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Current page", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_pl.arb b/app/lib/l10n/app_pl.arb index 28e8f8d5e004..d40c5659c7bf 100644 --- a/app/lib/l10n/app_pl.arb +++ b/app/lib/l10n/app_pl.arb @@ -11,7 +11,13 @@ "systemTheme": "Użyj domyślnego motywu systemowego", "view": "Widok", "contentViewport": "Podgląd treści", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Ogranicz widok do pozytywnych współrzędnych", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Wyłączony", "canvas": "Płótno", "interface": "Interfejs", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Czułość", - "sensitivityHint": "Im wyższa wartość, tym bardziej wrażliwe dane wejściowe", "horizontal": "Poziomy", "vertical": "Pionowo", "plain": "Zwykłe", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Dokument", + "documentStates": "Document states", "camera": "Aparat", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Lewy dolny róg", "bottomRight": "Dolny prawy róg", "zoomPosition": "Pozycja sterowania powiększeniem", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Skrzynki", "manage": "Zarządzaj", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Input gestures", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Natywny pasek tytułu", "mode": "Mode", "syncMode": "Tryb synchronizacji", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Brak mobilnych", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Ręcznie", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Szukaj", "@search": { "description": "Search action" }, "properties": "Właściwości", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Przypnij", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Kierunek", "onlyAvailableLargerScreen": "Dostępne tylko na większych ekranach", "toolbarPosition": "Pozycja paska narzędzi", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Obróć", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Kolej nawigacyjna", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Wytnij", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Motyw platformy", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Pulpit", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "Serwery ICE", "collaboration": "Współpraca", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Web Socket", "iceServer": "Serwer ICE", @@ -1037,7 +1065,7 @@ "hideUI": "Ukryj interfejs użytkownika", "density": "Gęstość", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Kompaktowy", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Kontynuuj mimo to", "zoomControl": "Kontrola powiększenia", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Wysoki kontrast", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Ta wartość powinna być poprawnym numerem", "createAreas": "Utwórz obszary", "autosave": "Autosave", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Invert", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Rozmiar paska narzędzi", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Dodaj wszystkie", "onlyCurrentPage": "Tylko bieżąca strona", "smoothNavigation": "Gładka nawigacja", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Przełącznik panewki krawędziowej", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Użyj Android SAF", "exact": "Dokładny", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Wiersze paska narzędzi", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Badanie wskaźnika", "pressure": "Ciśnienie", "small": "Mały", @@ -1158,6 +1207,9 @@ "selectAll": "Zaznacz wszystkie", "overrideTools": "Zastąp narzędzia", "hideCursorWhileDrawing": "Ukryj kursor podczas rysowania", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Zainstalowane", "install": "Zainstaluj", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "Przy starcie", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Ekran główny", "lastNote": "Ostatnia notatka", "newNote": "Nowa nota", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignoruj nacisk", - "ignoreFirstPressureDescription": "Na niektórych urządzeniach pierwsza wartość ciśnienia nie jest dokładna. To ustawienie zignoruje pierwszą wartość nacisku i zamiast tego użyj nacisku drugiego zdarzenia.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Tymczasowy", "simpleToolbarVisibility": "Widoczność prostego paska narzędzi", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Opóźnienie automatycznego zapisu", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Zapisano", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Przynieś elementy do przodu", "addTool": "Dodaj narzędzie", "nextPage": "Następna strona", - "previousPage": "Poprzednia strona" + "previousPage": "Poprzednia strona", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Bieżąca strona", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_pt.arb b/app/lib/l10n/app_pt.arb index 09c4b336969b..b5145da33376 100644 --- a/app/lib/l10n/app_pt.arb +++ b/app/lib/l10n/app_pt.arb @@ -11,7 +11,13 @@ "systemTheme": "Usar tema padrão do sistema", "view": "Visualizar", "contentViewport": "Visualização de conteúdo", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Limitar visualização a coordenadas positivas", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Desligado", "canvas": "Tela", "interface": "Interface", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Sensibilidade", - "sensitivityHint": "Quanto maior o valor, mais sensível à entrada", "horizontal": "Horizontal", "vertical": "Vertical", "plain": "Simples", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Documento", + "documentStates": "Document states", "camera": "Câmara", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Inferior esquerdo", "bottomRight": "Inferior direito", "zoomPosition": "Posição de controle", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Arquivos temporários", "manage": "Gerir", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Gestos de Entrada", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Barra de título nativa", "mode": "Mode", "syncMode": "Modo de sincronização", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Sem dados móveis", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Manualmente", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Pesquisa", "@search": { "description": "Search action" }, "properties": "Propriedades", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Fixar", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Direção", "onlyAvailableLargerScreen": "Disponível apenas em telas maiores", "toolbarPosition": "Posição da barra", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Rodar", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Trilho de navegação", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Recortar", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Tema da plataforma", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Computadores", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "Servidores ICE", "collaboration": "Colaboração", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Encaixe Web", "iceServer": "Servidor ICE", @@ -1037,7 +1065,7 @@ "hideUI": "Ocultar a IU", "density": "Densidade", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Compacta", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Continuar mesmo assim", "zoomControl": "Controlo de ampliação", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Alto contraste", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Este valor deve ser um número válido", "createAreas": "Criar áreas", "autosave": "Autosave", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Invert", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Tamanho da barra", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Adicionar tudo", "onlyCurrentPage": "Somente a página atual", "smoothNavigation": "Navegação suave", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Área da borda pan mudando", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Usar SAF Android", "exact": "Exato", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Barra de ferramentas", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Teste de ponteiro", "pressure": "Pressão", "small": "Pequeno", @@ -1158,6 +1207,9 @@ "selectAll": "Selecionar todos", "overrideTools": "Sobrescrever ferramentas", "hideCursorWhileDrawing": "Ocultar cursor durante o desenho", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Instalado", "install": "Instale", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "Na inicialização", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Tela inicial", "lastNote": "Última anotação", "newNote": "Nova nota", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignorar pressão", - "ignoreFirstPressureDescription": "Em alguns dispositivos, o primeiro valor de pressão não é exacto. Esta configuração ignorará o primeiro valor de pressão e usará, em vez disso, a pressão do segundo evento.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Temporário", "simpleToolbarVisibility": "Visibilidade na barra de ferramentas simples", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Auto-salvar atraso", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Salvo", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Trazer elementos movidos para a frente", "addTool": "Adicionar ferramenta", "nextPage": "Página seguinte", - "previousPage": "Página anterior" + "previousPage": "Página anterior", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Página atual", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_pt_BR.arb b/app/lib/l10n/app_pt_BR.arb index ff528df2cf69..1ec5eb0984e3 100644 --- a/app/lib/l10n/app_pt_BR.arb +++ b/app/lib/l10n/app_pt_BR.arb @@ -11,7 +11,13 @@ "systemTheme": "Usar tema padrão do sistema", "view": "Visualizar", "contentViewport": "Visualização de conteúdo", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Limitar visualização a coordenadas positivas", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Desligado", "canvas": "Tela", "interface": "Interface", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Sensibilidade", - "sensitivityHint": "Quanto maior o valor, mais sensível o valor de entrada", "horizontal": "Horizontal", "vertical": "Vertical", "plain": "Simples", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Documento", + "documentStates": "Document states", "camera": "Câmera", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Canto inferior esquerdo", "bottomRight": "Canto inferior direito", "zoomPosition": "Posição de controle", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Caches", "manage": "Administrar", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Gestos de entrada", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Barra de título nativa", "mode": "Mode", "syncMode": "Modo de sincronização", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Sem celular", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Manualmente", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Pesquisa", "@search": { "description": "Search action" }, "properties": "Propriedades", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "PIN", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Direção", "onlyAvailableLargerScreen": "Disponível apenas em telas maiores", "toolbarPosition": "Posição da barra", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Rotacionar", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Trilho de navegação", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Recortar", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Tema da plataforma", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Computadores", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "Servidores ICE", "collaboration": "Colaboração", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Encaixe Web", "iceServer": "Servidor ICE", @@ -1037,7 +1065,7 @@ "hideUI": "Ocultar interface", "density": "Densidade", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Compacta", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Continuar mesmo assim", "zoomControl": "Controle de zoom", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Alto contraste", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Este valor deve ser um número válido", "createAreas": "Criar áreas", "autosave": "Autosave", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Invert", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Tamanho da barra", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Adicionar tudo", "onlyCurrentPage": "Somente a página atual", "smoothNavigation": "Navegação suave", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Área da borda pan mudando", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Usar SAF Android", "exact": "Exato", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Barra de ferramentas", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Teste de ponteiro", "pressure": "Pressão", "small": "Pequeno", @@ -1158,6 +1207,9 @@ "selectAll": "Selecionar todos", "overrideTools": "Sobrescrever ferramentas", "hideCursorWhileDrawing": "Ocultar cursor durante o desenho", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Instalado", "install": "Instale", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "Na inicialização", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Tela inicial", "lastNote": "Última anotação", "newNote": "Nova nota", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignorar pressão", - "ignoreFirstPressureDescription": "Em alguns dispositivos, o primeiro valor de pressão não é exacto. Esta configuração ignorará o primeiro valor de pressão e usará, em vez disso, a pressão do segundo evento.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Temporário", "simpleToolbarVisibility": "Visibilidade na barra de ferramentas simples", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Auto-salvar atraso", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Salvo", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Trazer elementos movidos para a frente", "addTool": "Adicionar ferramenta", "nextPage": "Página seguinte", - "previousPage": "Página anterior" + "previousPage": "Página anterior", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Página atual", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_ro.arb b/app/lib/l10n/app_ro.arb index d03ec2b22cd0..5bc165f46ea4 100644 --- a/app/lib/l10n/app_ro.arb +++ b/app/lib/l10n/app_ro.arb @@ -11,7 +11,13 @@ "systemTheme": "Foloseste tema de sistem implicita", "view": "Vizualizare", "contentViewport": "Vizualizare conținut", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Vizualizare Limită la coordonate pozitive", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Dezactivat", "canvas": "Pânză", "interface": "Interfață", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Sensibilitate", - "sensitivityHint": "Cu cât este mai mare valoarea, cu atât mai sensibilă este aportul", "horizontal": "Orizontal", "vertical": "Verticală", "plain": "Simplu", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Document", + "documentStates": "Document states", "camera": "Cameră", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Stânga jos", "bottomRight": "Jos dreapta", "zoomPosition": "Poziție control zoom", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Geocutii", "manage": "Gestionează", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Input gestures", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Bară titlu nativ", "mode": "Mode", "syncMode": "Mod sincronizare", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Niciun mobil", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Manual", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Caută", "@search": { "description": "Search action" }, "properties": "Proprietăți", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Fixează", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Direcție", "onlyAvailableLargerScreen": "Disponibil doar pe ecrane mai mari", "toolbarPosition": "Poziția barei de instrumente", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Rotire", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Cale de navigare", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Taie", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Tema platformei", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Birou", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "Servere ICE", "collaboration": "Colaborare", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Socket web", "iceServer": "Server ICE", @@ -1037,7 +1065,7 @@ "hideUI": "Ascunde UI", "density": "Densitate", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Compact", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Continuă oricum", "zoomControl": "Comandă mărire", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Contrast mare", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Această valoare trebuie să fie un număr valid", "createAreas": "Creează zone", "autosave": "Autosave", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Invert", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Dimensiunea barei de instrumente", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Adaugă toate", "onlyCurrentPage": "Doar pagina curentă", "smoothNavigation": "Navigare lină", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Comutarea zonei panoului de margini", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Folosește Android SAF", "exact": "Exact", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Rânduri din bara de instrumente", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Testul indicatorului", "pressure": "Presiune", "small": "Mică", @@ -1158,6 +1207,9 @@ "selectAll": "Selectează tot", "overrideTools": "Suprascrie uneltele", "hideCursorWhileDrawing": "Ascunde cursorul în timpul desenării", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Instalat", "install": "Instalează", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "La pornire", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Ecranul principal", "lastNote": "Ultima notă", "newNote": "Notă nouă", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignoră presiunea", - "ignoreFirstPressureDescription": "Pe unele dispozitive, prima valoare a presiunii nu este exactă. Această setare va ignora prima valoare de presiune şi va folosi presiunea celui de-al doilea eveniment.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Temporar", "simpleToolbarVisibility": "Vizibilitate simplă a barei de instrumente", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Întârziere salvare automată", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Salvat", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Aduce elementele mutate în față", "addTool": "Adăugare unealtă", "nextPage": "Pagina următoare", - "previousPage": "Pagina precedentă" + "previousPage": "Pagina precedentă", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Pagina curentă", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_ru.arb b/app/lib/l10n/app_ru.arb index ce72937dd49e..ab985d9c8713 100644 --- a/app/lib/l10n/app_ru.arb +++ b/app/lib/l10n/app_ru.arb @@ -11,7 +11,13 @@ "systemTheme": "Использовать системную тему по умолчанию", "view": "Вид", "contentViewport": "Вид контента", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Ограничить видовые координаты", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Выкл", "canvas": "Холст", "interface": "Интерфейс", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Чувствительность", - "sensitivityHint": "Чем больше значение, тем чувствительнее ввод", "horizontal": "Горизонтально", "vertical": "Вертикально", "plain": "Обычный", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Документ", + "documentStates": "Document states", "camera": "Снимок", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Снизу слева", "bottomRight": "Снизу справа", "zoomPosition": "Управлять масштабом", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Кеш", "manage": "Управлять", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Жесты ввода", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Родной заголовок", "mode": "Mode", "syncMode": "Режим синхронизации", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Нет мобильных устройств", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Ручной", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Искать", "@search": { "description": "Search action" }, "properties": "Свойства", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Закрепить", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Направление", "onlyAvailableLargerScreen": "Доступно только на больших экранах", "toolbarPosition": "Положение панели инструментов", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Повернуть", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Навигационные рельсы", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Вырезать", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Тема платформы", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Рабочий стол", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "ICE серверы", "collaboration": "Сотрудничество", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Веб-сокет", "iceServer": "ICE сервер", @@ -1037,7 +1065,7 @@ "hideUI": "Скрыть UI", "density": "Плотность", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Компактный", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Все равно продолжить", "zoomControl": "Управление масштабом", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Высокий контраст", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Это значение должно быть действительным числом", "createAreas": "Создать области", "autosave": "Autosave", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Invert", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Размер панели инструментов", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Добавить все", "onlyCurrentPage": "Только текущая страница", "smoothNavigation": "Гладкая навигация", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Переключение области края пружины", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Использовать Android SAF", "exact": "Точно", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Строки панели", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Тест указателя", "pressure": "Давление", "small": "Маленький", @@ -1158,6 +1207,9 @@ "selectAll": "Выбрать все", "overrideTools": "Переопределить инструменты", "hideCursorWhileDrawing": "Скрыть курсор во время рисования", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Установлено", "install": "Установить", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "При запуске", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Домашний экран", "lastNote": "Последняя заметка", "newNote": "Новая заметка", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Игнорировать давление", - "ignoreFirstPressureDescription": "На некоторых устройствах первое значение давления неточно. Эта настройка будет игнорировать первое значение давления и вместо этого использовать давление второго события.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Временно", "simpleToolbarVisibility": "Простая видимость панели инструментов", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Задержка автосохранения", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Сохранено", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Принести перемещенные элементы на передний план", "addTool": "Добавить инструмент", "nextPage": "Следующая страница", - "previousPage": "Предыдущая страница" + "previousPage": "Предыдущая страница", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Текущая страница", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_sr.arb b/app/lib/l10n/app_sr.arb index 418b1370b9d5..3862ca1e6b04 100644 --- a/app/lib/l10n/app_sr.arb +++ b/app/lib/l10n/app_sr.arb @@ -11,7 +11,13 @@ "systemTheme": "Koristi podrazumevanu sistemsku temu", "view": "Prikaz", "contentViewport": "Content Viewport", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Limit Viewport to positive coordinates", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Off", "canvas": "Canvas", "interface": "Interface", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Osetljivost", - "sensitivityHint": "Što je veća vrednost, to je unos osetljiviji", "horizontal": "Horizontalno", "vertical": "Vertikalno", "plain": "Obično", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Dokument", + "documentStates": "Document states", "camera": "Kamera", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Dole levo", "bottomRight": "Dole desno", "zoomPosition": "Zoom control position", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Keš memorije", "manage": "Upravljaj", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Pokreti unosa", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Nativna naslovna traka", "mode": "Mode", "syncMode": "Režim sinhronizacije", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Nema na mobilnom", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Ručno", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Pretraga", "@search": { "description": "Search action" }, "properties": "Svojstva", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Zakači", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Direction", "onlyAvailableLargerScreen": "Dostupno samo na većim ekranima", "toolbarPosition": "Pozicija trake sa alatkama", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Rotiraj", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Navigaciona traka", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Iseci", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Tema platforme", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Desktop", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "ICE serveri", "collaboration": "Saradnja", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Web Socket", "iceServer": "ICE Server", @@ -1037,7 +1065,7 @@ "hideUI": "Sakrij interfejs", "density": "Gustina", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Kompaktno", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Nastavi svejedno", "zoomControl": "Kontrola zumiranja", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Visok kontrast", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Ova vrednost treba da bude važeći broj", "createAreas": "Kreiraj oblasti", "autosave": "Automatsko čuvanje", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Invertuj", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Veličina trake sa alatkama", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Dodaj sve", "onlyCurrentPage": "Samo trenutna stranica", "smoothNavigation": "Glatka navigacija", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Edge pan area switching", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Use Android SAF", "exact": "Tačno", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Redovi trake sa alatkama", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Test pokazivača", "pressure": "Pritisak", "small": "Malo", @@ -1158,6 +1207,9 @@ "selectAll": "Izaberi sve", "overrideTools": "Prepiši alate", "hideCursorWhileDrawing": "Sakrij kursor tokom crtanja", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Instalirano", "install": "Instaliraj", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "Pri pokretanju", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Početni ekran", "lastNote": "Poslednja beleška", "newNote": "Nova beleška", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignore pressure", - "ignoreFirstPressureDescription": "On some devices, the first pressure value is not accurate. This setting will ignore the first pressure value and use the pressure of the second event instead.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Temporary", "simpleToolbarVisibility": "Simple toolbar visibility", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Autosave delay", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Saved", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Bring moved elements to front", "addTool": "Add tool", "nextPage": "Next page", - "previousPage": "Previous page" + "previousPage": "Previous page", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Current page", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_sv.arb b/app/lib/l10n/app_sv.arb index 7d8c2f32494d..fe67e0c66c5f 100644 --- a/app/lib/l10n/app_sv.arb +++ b/app/lib/l10n/app_sv.arb @@ -11,7 +11,13 @@ "systemTheme": "Använd standard systemtema", "view": "Visa", "contentViewport": "Visningsport för innehåll", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Begränsa Viewport till positiva koordinater", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Av", "canvas": "Täcke", "interface": "Gränsgränssnitt", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Känslighet", - "sensitivityHint": "Ju högre värde, desto känsligare är inmatningen", "horizontal": "Horisontell", "vertical": "Vertikal", "plain": "Oformaterad", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Dokument", + "documentStates": "Document states", "camera": "Kamera", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Nere till vänster", "bottomRight": "Nere till höger", "zoomPosition": "Zooma kontrollposition", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Cacher", "manage": "Hantera", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Input gestures", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Namnfält med ursprungsbeteckning", "mode": "Mode", "syncMode": "Synkronisera läge", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Ingen mobil", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Manuell", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Sök", "@search": { "description": "Search action" }, "properties": "Egenskaper", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Fäst", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Riktning", "onlyAvailableLargerScreen": "Finns endast på större skärmar", "toolbarPosition": "Verktygsfältets position", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Rotera", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Navigeringsräls", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Klipp", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Plattformens tema", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Skrivbord", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "ICE servrar", "collaboration": "Samarbete", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Webb Socket", "iceServer": "ICE Server", @@ -1037,7 +1065,7 @@ "hideUI": "Dölj gränssnitt", "density": "Densitet", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Kompakt", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Fortsätt ändå", "zoomControl": "Zooma kontroll", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Hög kontrast", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Detta värde bör vara giltigt nummer", "createAreas": "Skapa områden", "autosave": "Autosave", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Invert", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Storlek på verktygsfält", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Lägg till alla", "onlyCurrentPage": "Endast aktuell sida", "smoothNavigation": "Smidig navigering", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Kanten panorering område växla", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Använd Android SAF", "exact": "Exakt", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Verktygsfält rader", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Pekare test", "pressure": "Tryck", "small": "Liten", @@ -1158,6 +1207,9 @@ "selectAll": "Markera alla", "overrideTools": "Åsidosätt verktyg", "hideCursorWhileDrawing": "Dölj markören medan du ritar", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Installerad", "install": "Installera", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "Vid uppstart", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Startskärmen", "lastNote": "Senaste anteckning", "newNote": "Ny anteckning", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignorera tryck", - "ignoreFirstPressureDescription": "På vissa enheter är det första tryckvärdet inte korrekt. Denna inställning kommer att ignorera det första tryckvärdet och använda trycket från den andra händelsen istället.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Tillfällig", "simpleToolbarVisibility": "Enkel synlighet i verktygsfältet", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Fördröjning för automatisk sparning", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Sparad", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Ta med flyttade element framtill", "addTool": "Lägg till verktyg", "nextPage": "Nästa sida", - "previousPage": "Föregående sida" + "previousPage": "Föregående sida", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Nuvarande sida", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_th.arb b/app/lib/l10n/app_th.arb index 0e20d8f6eab0..4ebcf8cf1404 100644 --- a/app/lib/l10n/app_th.arb +++ b/app/lib/l10n/app_th.arb @@ -11,7 +11,13 @@ "systemTheme": "ใช้ธีมระบบเริ่มต้น", "view": "ดู", "contentViewport": "Content Viewport", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Limit Viewport to positive coordinates", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Off", "canvas": "Canvas", "interface": "Interface", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "ความไว", - "sensitivityHint": "ค่ายิ่งสูง อินพุตจะยิ่งไว", "horizontal": "แนวนอน", "vertical": "แนวตั้ง", "plain": "ไม่มีเส้น", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "เอกสาร", + "documentStates": "Document states", "camera": "กล้อง", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "ล่างซ้าย", "bottomRight": "ล่างขวา", "zoomPosition": "Zoom control position", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "แคช", "manage": "จัดการ", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "การทำท่าทางอินพุต", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "แถบหัวเรื่องเริ่มต้น", "mode": "Mode", "syncMode": "โหมดซิงค์", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "ไม่ใช้งานบนมือถือ", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "ด้วยตนเอง", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "ค้นหา", "@search": { "description": "Search action" }, "properties": "คุณสมบัติ", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "ปักหมุด", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Direction", "onlyAvailableLargerScreen": "ใช้ได้เฉพาะบนหน้าจอขนาดใหญ่", "toolbarPosition": "ตำแหน่งแถบเครื่องมือ", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "หมุน", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "รางนำทาง", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "ตัด", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "ธีมแพลตฟอร์ม", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "เดสก์ท็อป", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "เซิร์ฟเวอร์ ICE", "collaboration": "ความร่วมมือ", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Web Socket", "iceServer": "เซิร์ฟเวอร์ ICE", @@ -1037,7 +1065,7 @@ "hideUI": "ซ่อน UI", "density": "ความหนาแน่น", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "กะทัดรัด", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "ดำเนินการต่ออย่างไรก็ตาม", "zoomControl": "ตัวควบคุมการซูม", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "คอนทราสต์สูง", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "ค่านี้ควรเป็นตัวเลขที่ถูกต้อง", "createAreas": "สร้างพื้นที่", "autosave": "บันทึกอัตโนมัติ", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "กลับ", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "ขนาดแถบเครื่องมือ", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "เพิ่มทั้งหมด", "onlyCurrentPage": "เฉพาะหน้าปัจจุบัน", "smoothNavigation": "การนำทางเรียบ", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Edge pan area switching", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Use Android SAF", "exact": "แม่นยำ", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "แถวของแถบเครื่องมือ", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "ทดสอบตัวชี้", "pressure": "ความดัน", "small": "เล็ก", @@ -1158,6 +1207,9 @@ "selectAll": "เลือกทั้งหมด", "overrideTools": "แทนที่เครื่องมือ", "hideCursorWhileDrawing": "ซ่อนเคอร์เซอร์ขณะวาด", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "ติดตั้งแล้ว", "install": "ติดตั้ง", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "เมื่อเริ่มต้น", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "หน้าจอหลัก", "lastNote": "บันทึกล่าสุด", "newNote": "บันทึกใหม่", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignore pressure", - "ignoreFirstPressureDescription": "On some devices, the first pressure value is not accurate. This setting will ignore the first pressure value and use the pressure of the second event instead.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Temporary", "simpleToolbarVisibility": "Simple toolbar visibility", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Autosave delay", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Saved", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Bring moved elements to front", "addTool": "Add tool", "nextPage": "Next page", - "previousPage": "Previous page" + "previousPage": "Previous page", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Current page", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_tr.arb b/app/lib/l10n/app_tr.arb index 45f3c34fba80..8a50eab2b816 100644 --- a/app/lib/l10n/app_tr.arb +++ b/app/lib/l10n/app_tr.arb @@ -11,7 +11,13 @@ "systemTheme": "Varsayılan sistem temasını kullan", "view": "Görüntüle", "contentViewport": "Content Viewport", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Limit Viewport to positive coordinates", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Off", "canvas": "Canvas", "interface": "Interface", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Hassasiyet", - "sensitivityHint": "Değer arttıkça hassasiyet artar", "horizontal": "Yatay", "vertical": "Dikey", "plain": "Sade", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Belge", + "documentStates": "Document states", "camera": "Kamera", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Sol alt", "bottomRight": "Sağ alt", "zoomPosition": "Zoom control position", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Önbellek", "manage": "Yönet", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Giriş hareketleri", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Yerel başlık çubuğu", "mode": "Mode", "syncMode": "Eşitleme kipi", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Mobil olmayan", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Kullanım kılavuzu", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Ara", "@search": { "description": "Search action" }, "properties": "Özellikler", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Sabitle", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Direction", "onlyAvailableLargerScreen": "Sadece daha büyük ekranlarda kullanılabilir", "toolbarPosition": "Araç çubuğu konumu", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Döndür", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Gezinme rayı", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Kes", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Platform teması", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Masaüstü", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "ICE Sunucuları", "collaboration": "İş birliği", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Web Soket", "iceServer": "ICE Sunucusu", @@ -1037,7 +1065,7 @@ "hideUI": "Arayüzü Gizle", "density": "Yoğunluk", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Kompakt", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Yine de devam et", "zoomControl": "Yakınlaştırma kontrolü", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Yüksek kontrast", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Bu değer geçerli bir sayı olmalıdır", "createAreas": "Alanlar oluştur", "autosave": "Otomatik kaydetme", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Ters çevir", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Araç çubuğu boyutu", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Tümünü ekle", "onlyCurrentPage": "Sadece geçerli sayfa", "smoothNavigation": "Akıcı gezinme", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Edge pan area switching", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Use Android SAF", "exact": "Tam", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Araç çubuğu satırları", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "İşaretçi testi", "pressure": "Basınç", "small": "Küçük", @@ -1158,6 +1207,9 @@ "selectAll": "Tümünü seç", "overrideTools": "Araçları geçersiz kıl", "hideCursorWhileDrawing": "Çizim yaparken imleci gizle", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Yüklü", "install": "Yükle", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "Başlangıçta", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Ana ekran", "lastNote": "Son not", "newNote": "Yeni not", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignore pressure", - "ignoreFirstPressureDescription": "On some devices, the first pressure value is not accurate. This setting will ignore the first pressure value and use the pressure of the second event instead.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Temporary", "simpleToolbarVisibility": "Simple toolbar visibility", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Autosave delay", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Saved", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Bring moved elements to front", "addTool": "Add tool", "nextPage": "Next page", - "previousPage": "Previous page" + "previousPage": "Previous page", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Current page", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_uk.arb b/app/lib/l10n/app_uk.arb index 76e835adfbf9..9875e05b60b9 100644 --- a/app/lib/l10n/app_uk.arb +++ b/app/lib/l10n/app_uk.arb @@ -11,7 +11,13 @@ "systemTheme": "Використовувати типову системну тему", "view": "Дивитись", "contentViewport": "Перегляд вмісту", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Обмежити Перегляд порту до позитивних координат", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Вимкнено", "canvas": "Полотно", "interface": "Інтерфейс", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Чутливість", - "sensitivityHint": "Чим вище значення, тим більш чутливе вхідне значення", "horizontal": "Горизонтально", "vertical": "Вертикально", "plain": "Рівнина", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Документ", + "documentStates": "Document states", "camera": "Камера", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Внизу ліворуч", "bottomRight": "Нижній правий", "zoomPosition": "Позиція керування масштабом", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Кеш", "manage": "Керування", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Input gestures", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Область заголовка", "mode": "Mode", "syncMode": "Режим синхронізації", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Немає мобільного телефону", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Вручну", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Пошук", "@search": { "description": "Search action" }, "properties": "Властивості", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Закріплення повідомлень", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Напрямок", "onlyAvailableLargerScreen": "Доступно тільки на більших екранах", "toolbarPosition": "Позиція панелі інструментів", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Обертати", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Навігаційна залізниця", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Вирізати", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Тема платформи", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Стільниця", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "ICE сервери", "collaboration": "Співпраця", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Веб-сокет", "iceServer": "Сервер ICE", @@ -1037,7 +1065,7 @@ "hideUI": "Приховати інтерфейс", "density": "Густина", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Компактний", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Все одно продовжити", "zoomControl": "Управління масштабуванням", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Високий контраст", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Це значення має бути дійсним числом", "createAreas": "Створення області", "autosave": "Autosave", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Invert", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Розмір панелі інструментів", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Додати всі", "onlyCurrentPage": "Тільки поточна сторінка", "smoothNavigation": "Плавна навігація", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Вимикання області pan", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Використовувати Android SAF", "exact": "Точно", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Рядки панелі інструментів", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Тест вказівника", "pressure": "Тиск", "small": "Маленький", @@ -1158,6 +1207,9 @@ "selectAll": "Виділити все", "overrideTools": "Змінити інструменти", "hideCursorWhileDrawing": "Сховати курсор під час малювання", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Встановлено", "install": "Інсталювати", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "При запуску", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Домашній екран", "lastNote": "Остання примітка", "newNote": "Нова нотатка", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ігнорувати тиск", - "ignoreFirstPressureDescription": "На деяких пристроях перше значення тиску не точне. Ця настройка ігноруватиме перше значення тиску і буде використовувати тиск другої події.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Тимчасово", "simpleToolbarVisibility": "Проста видимість панелі інструментів", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Затримка автозбереження", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Збережено", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Принести переміщені елементи на передній план", "addTool": "Додати інструмент", "nextPage": "Наступна сторінка", - "previousPage": "Попередня сторінка" + "previousPage": "Попередня сторінка", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Поточна сторінка", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_vi.arb b/app/lib/l10n/app_vi.arb index 93ddf4539c21..a51068b390b0 100644 --- a/app/lib/l10n/app_vi.arb +++ b/app/lib/l10n/app_vi.arb @@ -11,7 +11,13 @@ "systemTheme": "Dùng giao diện mặc định của máy", "view": "Hiển thị", "contentViewport": "Content Viewport", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Limit Viewport to positive coordinates", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Off", "canvas": "Canvas", "interface": "Interface", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "Độ nhạy", - "sensitivityHint": "Giá trị càng cao, đầu vào càng nhạy", "horizontal": "Ngang", "vertical": "Dọc", "plain": "Trơn", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "Tài liệu", + "documentStates": "Document states", "camera": "Máy ảnh", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "Dưới trái", "bottomRight": "Dưới phải", "zoomPosition": "Vị trí điều khiển thu phóng", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "Bộ nhớ đệm", "manage": "Quản lý", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "Cử chỉ đầu vào", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "Thanh tiêu đề gốc", "mode": "Mode", "syncMode": "Chế độ đồng bộ", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "Không dùng dữ liệu di động", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "Thủ công", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "Tìm kiếm", "@search": { "description": "Search action" }, "properties": "Thuộc tính", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "Ghim", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Direction", "onlyAvailableLargerScreen": "Chỉ khả dụng trên màn hình lớn hơn", "toolbarPosition": "Vị trí thanh công cụ", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "Xoay", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "Thanh điều hướng", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "Cắt", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "Giao diện nền tảng", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "Máy tính", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "Máy chủ ICE", "collaboration": "Cộng tác", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Web Socket", "iceServer": "Máy chủ ICE", @@ -1037,7 +1065,7 @@ "hideUI": "Ẩn giao diện", "density": "Mật độ", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "Gọn", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "Tiếp tục", "zoomControl": "Điều khiển thu phóng", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "Độ tương phản cao", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "Giá trị này phải là số hợp lệ", "createAreas": "Tạo khu vực", "autosave": "Tự động lưu", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "Đảo ngược", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "Kích thước thanh công cụ", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "Thêm tất cả", "onlyCurrentPage": "Chỉ trang hiện tại", "smoothNavigation": "Điều hướng mượt", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Edge pan area switching", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Use Android SAF", "exact": "Chính xác", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "Số hàng thanh công cụ", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "Kiểm tra con trỏ", "pressure": "Áp lực", "small": "Nhỏ", @@ -1158,6 +1207,9 @@ "selectAll": "Chọn tất cả", "overrideTools": "Ghi đè công cụ", "hideCursorWhileDrawing": "Ẩn con trỏ khi vẽ", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "Đã cài", "install": "Cài đặt", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "Khi khởi động", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "Màn hình chính", "lastNote": "Ghi chú cuối", "newNote": "Ghi chú mới", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Bỏ qua áp lực", - "ignoreFirstPressureDescription": "Trên một số thiết bị, giá trị áp suất đầu tiên không chính xác. Cài đặt này sẽ bỏ qua giá trị áp suất đầu tiên và thay vào đó sử dụng áp suất của sự kiện thứ hai.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Tạm thời", "simpleToolbarVisibility": "Khả năng hiển thị thanh công cụ đơn giản", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Độ trễ tự động lưu", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Đã lưu", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Bring moved elements to front", "addTool": "Add tool", "nextPage": "Next page", - "previousPage": "Previous page" + "previousPage": "Previous page", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Current page", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_zh-Hant.arb b/app/lib/l10n/app_zh-Hant.arb index 879cb14c6f08..cd60b2a9132e 100644 --- a/app/lib/l10n/app_zh-Hant.arb +++ b/app/lib/l10n/app_zh-Hant.arb @@ -11,7 +11,13 @@ "systemTheme": "使用系統預設主題", "view": "查看", "contentViewport": "Content Viewport", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "Limit Viewport to positive coordinates", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "Off", "canvas": "Canvas", "interface": "Interface", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "靈敏度", - "sensitivityHint": "數值越高,輸入越靈敏", "horizontal": "水平", "vertical": "垂直", "plain": "素色", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "文件", + "documentStates": "Document states", "camera": "鏡頭", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "左下角", "bottomRight": "右下角", "zoomPosition": "Zoom control position", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "快取", "manage": "管理", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "手勢輸入", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "原生標題列", "mode": "Mode", "syncMode": "同步模式", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "沒有行動裝置", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "手動", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "搜尋", "@search": { "description": "Search action" }, "properties": "屬性", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "釘選", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "Direction", "onlyAvailableLargerScreen": "僅適用於大螢幕設備", "toolbarPosition": "工具列位置", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "旋轉", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "導覽列", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "剪下", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "平台主題", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "電腦", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "ICE 伺服器", "collaboration": "協作", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "", "webSocket": "WebSocket", "iceServer": "ICE 伺服器", @@ -1037,7 +1065,7 @@ "hideUI": "隱藏介面", "density": "密度", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "緊湊", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "仍然繼續", "zoomControl": "縮放控制", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "高對比", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "此值應為有效數字", "createAreas": "建立分區", "autosave": "自動儲存", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "反轉", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "工具列大小", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "全部新增", "onlyCurrentPage": "僅本頁", "smoothNavigation": "流暢導覽", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "Edge pan area switching", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "Use Android SAF", "exact": "精確", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "工具列列數", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "指標測試", "pressure": "壓力", "small": "小", @@ -1158,6 +1207,9 @@ "selectAll": "全選", "overrideTools": "覆蓋工具", "hideCursorWhileDrawing": "繪製時隱藏游標", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "已安裝", "install": "安裝", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "啟動時", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "主畫面", "lastNote": "上一則筆記", "newNote": "新筆記", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "Ignore pressure", - "ignoreFirstPressureDescription": "On some devices, the first pressure value is not accurate. This setting will ignore the first pressure value and use the pressure of the second event instead.", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "Temporary", "simpleToolbarVisibility": "Simple toolbar visibility", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "Autosave delay", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "Saved", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "Bring moved elements to front", "addTool": "Add tool", "nextPage": "Next page", - "previousPage": "Previous page" + "previousPage": "Previous page", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "Current page", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file diff --git a/app/lib/l10n/app_zh.arb b/app/lib/l10n/app_zh.arb index a75df6f1d6a7..9813d5529f88 100644 --- a/app/lib/l10n/app_zh.arb +++ b/app/lib/l10n/app_zh.arb @@ -11,7 +11,13 @@ "systemTheme": "使用系统默认主题", "view": "查看", "contentViewport": "内容视图", + "@contentViewport": { + "description": "Limit how far the viewport can move beyond the content" + }, "limitViewportToPositiveCoordinates": "限制视图为正坐标", + "@limitViewportToPositiveCoordinates": { + "description": "Prevent the viewport from moving into negative coordinates" + }, "off": "关闭", "canvas": "画布", "interface": "接口", @@ -316,7 +322,6 @@ "description": "SVG format" }, "sensitivity": "灵敏度", - "sensitivityHint": "值越高,输入越敏感", "horizontal": "水平", "vertical": "垂直的", "plain": "纯色", @@ -356,6 +361,7 @@ "description": "Insert action" }, "document": "文件", + "documentStates": "Document states", "camera": "摄像头", "@camera": { "description": "Camera tool" @@ -577,6 +583,9 @@ "bottomLeft": "左下", "bottomRight": "右下", "zoomPosition": "缩放控制位置", + "@zoomPosition": { + "description": "Choose where the zoom controls appear" + }, "caches": "缓存", "manage": "管理", "@manage": { @@ -636,6 +645,7 @@ "description": "Setting to show pen only toggle button when pen is detected" }, "inputGestures": "输入手势", + "inputGesturesDescription": "Lets you move and zoom the canvas with touch gestures, even while a drawing tool is selected.", "nativeTitleBar": "原生标题栏", "mode": "Mode", "syncMode": "同步模式", @@ -645,15 +655,21 @@ "description": "Frequency" }, "noMobile": "无手机", + "syncModeAlwaysDescription": "Syncs remote files automatically whenever they change.", + "syncModeNoMobileDescription": "Syncs automatically except on mobile devices, which can reduce mobile data usage.", "manual": "手动模式", "@manual": { "description": "Manual mode" }, + "syncModeManualDescription": "Only syncs when you start it yourself.", "search": "搜索", "@search": { "description": "Search action" }, "properties": "属性", + "@properties": { + "description": "Choose where the properties panel appears" + }, "pin": "置顶", "@pin": { "description": "Pin action" @@ -942,6 +958,9 @@ "direction": "方向", "onlyAvailableLargerScreen": "仅在大屏幕上可用", "toolbarPosition": "工具栏位置", + "@toolbarPosition": { + "description": "Choose where the main toolbar appears" + }, "rotate": "旋转", "@rotate": { "description": "Rotate action" @@ -951,6 +970,9 @@ "description": "Spacer element" }, "navigationRail": "导航列", + "@navigationRail": { + "description": "Show a navigation rail on wider screens" + }, "cut": "剪切", "@cut": { "description": "Clipboard action" @@ -982,6 +1004,9 @@ "description": "Texture property" }, "platformTheme": "平台主题", + "@platformTheme": { + "description": "Choose whether the app follows the system, desktop, or mobile layout" + }, "desktop": "桌面", "@desktop": { "description": "Platform" @@ -1006,6 +1031,9 @@ }, "iceServers": "ICE 服务器", "collaboration": "合作", + "@collaboration": { + "description": "Allow multiple people to edit the same note together" + }, "webRtc": "Web RTC", "webSocket": "Web 套接字", "iceServer": "ICE 服务器", @@ -1037,7 +1065,7 @@ "hideUI": "隐藏界面", "density": "密度", "@density": { - "description": "Density property" + "description": "Choose how compact the interface should be" }, "compact": "紧凑的", "@compact": { @@ -1109,14 +1137,23 @@ }, "continueAnyway": "仍然继续", "zoomControl": "缩放控制", + "@zoomControl": { + "description": "Show zoom controls in the canvas view" + }, "showThumbnails": "Show thumbnails", "@showThumbnails": { "description": "Show thumbnails for notes in the file list" }, "highContrast": "高对比度", + "@highContrast": { + "description": "Use stronger contrast in the app theme" + }, "shouldANumber": "此值应该是有效的数字", "createAreas": "创建区域", "autosave": "自动保存", + "@autosave": { + "description": "Choose how changes are saved" + }, "invert": "反转", "@invert": { "description": "Invert action" @@ -1131,10 +1168,19 @@ "description": "Size value" }, "toolbarSize": "工具栏大小", + "@toolbarSize": { + "description": "Choose how large the toolbar buttons are" + }, "addAll": "添加全部", "onlyCurrentPage": "仅当前页面", "smoothNavigation": "平滑导航", + "@smoothNavigation": { + "description": "Reduce the amount of rendering work while navigating" + }, "edgePanAreaSwitching": "切换边框", + "@edgePanAreaSwitching": { + "description": "Switch areas when you pan near the edge of the canvas" + }, "useAndroidSaf": "使用 Android SAF", "exact": "精准的", "@exact": { @@ -1145,6 +1191,9 @@ "description": "Inline position" }, "toolbarRows": "工具栏行", + "@toolbarRows": { + "description": "Set how many rows the toolbar can use" + }, "pointerTest": "指针测试", "pressure": "压力", "small": "小的", @@ -1158,6 +1207,9 @@ "selectAll": "选择所有", "overrideTools": "覆盖工具", "hideCursorWhileDrawing": "绘制时隐藏光标", + "@hideCursorWhileDrawing": { + "description": "Hide the mouse cursor while drawing" + }, "installed": "已安装", "install": "安装", "@install": { @@ -1174,6 +1226,9 @@ "description": "Scroll action" }, "onStartup": "启动时", + "@onStartup": { + "description": "Choose what opens when the app starts" + }, "homeScreen": "主屏幕", "lastNote": "最后一个笔记", "newNote": "新建笔记", @@ -1301,10 +1356,21 @@ "description": "Math tool" }, "ignorePressure": "忽略压力", - "ignoreFirstPressureDescription": "在某些设备上,第一个压力值不准确。 此设置将忽略第一个压力值,然后使用第二个事件的压力。", + "@ignorePressure": { + "description": "Choose how stylus pressure is handled" + }, + "ignoreFirstPressureDescription": "Ignores the first pressure reading, which can be inaccurate on some pens.", + "ignorePressureNeverDescription": "Uses every pressure reading from the pen.", + "ignorePressureAlwaysDescription": "Ignores pressure and treats the pen as fully pressed.", "temporary": "临时的", "simpleToolbarVisibility": "简单工具栏可见性", + "@simpleToolbarVisibility": { + "description": "Choose when the simplified toolbar is shown" + }, "autosaveDelay": "自动保存延迟", + "@autosaveDelay": { + "description": "Wait time before delayed autosave runs" + }, "saved": "已保存", "@saved": { "description": "Save status" @@ -1371,5 +1437,67 @@ "bringMovedElementsToFront": "将移动元素带到前端", "addTool": "添加工具", "nextPage": "下一页", - "previousPage": "上一页" + "previousPage": "上一页", + "persistenceDocumentStates": "Persistent document states", + "persistentStatesEnabled": "Enable persistent document states", + "persistentStateCurrentPage": "当前页面", + "persistentStateViewport": "Viewport position and zoom", + "persistentStateSelectedTool": "Selected tool", + "persistentStateMaxRecords": "Maximum stored records", + "persistentStateDeleteOlderThanDays": "Delete records older than days", + "persistentStateCleanup": "Clean up stored states", + "persistentStateCleanupDescription": "Delete records older than the limits above", + "persistentStateCleanupInProgress": "Cleaning up...", + "persistentStateCleanupFeedback": "{locations, plural, =1{Cleaned up 1 storage location} other{Cleaned up {locations} storage locations}}; {records, plural, =1{removed 1 record} other{removed {records} records}}.", + "@persistentStateCleanupFeedback": { + "placeholders": { + "locations": { + "type": "int" + }, + "records": { + "type": "int" + } + } + }, + "autosaveEnabledDescription": "Saves changes automatically as you work.", + "autosaveDelayedDescription": "Saves changes automatically after the selected delay.", + "autosaveShowButtonDescription": "Saves automatically and also keeps the save button visible.", + "autosaveDisabledDescription": "Only saves when you use the save command.", + "hideCursorWhileDrawingDescription": "Hides the mouse pointer while you draw, so it does not cover your stroke.", + "onStartupHomeScreenDescription": "Opens the home screen when Butterfly starts.", + "onStartupLastNoteDescription": "Reopens your most recently used note when Butterfly starts.", + "onStartupNewNoteDescription": "Creates a new note when Butterfly starts.", + "smoothNavigationDescription": "Makes panning and zooming feel smoother by simplifying the canvas while it is moving.", + "edgePanAreaSwitchingDescription": "Switches to a neighboring area when you pan near the edge of the canvas.", + "collaborationDescription": "Allows multiple people to edit the same note together in real time.", + "showVerboseLogsDescription": "Includes detailed diagnostic messages in the log view and console. These messages can help when troubleshooting a problem.", + "bringMovedElementsToFrontDescription": "Places an element above overlapping elements after you move it.", + "persistenceDocumentStatesDescription": "Controls which parts of your workspace Butterfly remembers for each note, such as the current page, zoom, and selected tool.", + "persistentStatesEnabledDescription": "Store document state information between app sessions", + "persistentStateCurrentPageDescription": "Remember the current page when reopening a file", + "persistentStateViewportDescription": "Remember the viewport position and zoom", + "persistentStateSelectedToolDescription": "Remember the selected tool when reopening a file", + "persistentStateLocksDescription": "Remember layer and collection locks when reopening a file", + "persistentStateNavigatorDescription": "Remember the navigator state when reopening a file", + "persistentStateLayersDescription": "Remember the layer panel state when reopening a file", + "persistentStateAreasDescription": "Remember the area panel state when reopening a file", + "persistentStateMaxRecordsDescription": "Keep at most this many stored states per file", + "persistentStateDeleteOlderThanDaysDescription": "Delete stored states older than this many days", + "contentViewportDescription": "Limits how far you can pan away from the content of your note.", + "limitViewportPositiveDescription": "Keeps the canvas within the area to the right and below its starting point.", + "startInFullScreenDescription": "Opens Butterfly in full-screen mode when supported.", + "designDescription": "Changes the overall visual style of Butterfly without changing whether the app uses light or dark colors.", + "platformThemeDescription": "Lets you use the desktop or mobile layout instead of the layout Butterfly normally chooses for your device.", + "densityDescription": "Changes the spacing and size of interface elements. It does not change the size of your note content.", + "highContrastDescription": "Uses stronger color differences to make controls and text easier to distinguish.", + "nativeTitleBarDescription": "Uses your operating system's standard window title bar instead of Butterfly's custom title bar.", + "propertiesDescription": "Chooses where the panel for editing the selected tool or element appears.", + "navigationRailDescription": "Shows a permanent navigation bar on wide screens for quicker access to app sections.", + "optionsPanelPositionDescription": "Chooses whether tool options appear above or below the canvas.", + "simpleToolbarVisibilityDescription": "Controls when the smaller, simplified toolbar appears while editing a note.", + "penOnlyInputDescription": "Prevents accidental marks from your hand or mouse by choosing when only pen input can draw.", + "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", + "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", + "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." } \ No newline at end of file From 36476f8fbdf0525018ff52cba6752703954e3acf Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 13 Jul 2026 11:48:28 +0200 Subject: [PATCH 063/117] Fix dependencies --- SECURITY.md | 2 +- app/android/Gemfile.lock | 22 +-- docs/package.json | 2 +- docs/pnpm-lock.yaml | 281 ++++++--------------------------------- tools/pubspec.lock | 4 +- 5 files changed, 56 insertions(+), 255 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 1148af13e8f6..04ed32580b65 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ | Version | Supported | | | -------------------------- | ------------------ | ----------------------------------------------------------------------------- | -| 2.6-dev (Dreamy Duskywing) | :warning: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.6.0-beta.1) | +| 2.6-dev (Dreamy Duskywing) | :warning: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.6.0-beta.2) | | 2.5.3 (Crimson Red) | :white_check_mark: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.5.3) | | 2.4.4 (Black Hairstreak) | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.4.4) | | 2.3.4 (Adonis Blue) | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.3.4) | diff --git a/app/android/Gemfile.lock b/app/android/Gemfile.lock index dbcd9c66fba3..30dd4fc09095 100644 --- a/app/android/Gemfile.lock +++ b/app/android/Gemfile.lock @@ -8,8 +8,8 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1265.0) - aws-sdk-core (3.252.0) + aws-partitions (1.1268.0) + aws-sdk-core (3.254.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -17,11 +17,11 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.129.0) - aws-sdk-core (~> 3, >= 3.248.0) + aws-sdk-kms (1.130.0) + aws-sdk-core (~> 3, >= 3.254.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.226.0) - aws-sdk-core (~> 3, >= 3.248.0) + aws-sdk-s3 (1.227.0) + aws-sdk-core (~> 3, >= 3.254.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) @@ -148,7 +148,7 @@ GEM google-cloud-env (2.2.2) base64 (~> 0.2) faraday (>= 1.0, < 3.a) - google-cloud-errors (1.6.0) + google-cloud-errors (1.7.0) google-cloud-storage (1.62.0) addressable (~> 2.8) digest-crc (~> 0.4) @@ -173,7 +173,7 @@ GEM httpclient (2.9.0) mutex_m jmespath (1.6.2) - json (2.20.0) + json (2.21.1) jwt (3.2.0) base64 logger (1.7.0) @@ -222,12 +222,14 @@ GEM uber (0.1.0) unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.27.0) + xcodeproj (1.28.1) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) + base64 claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.4.0) + nkf rexml (>= 3.3.6, < 4.0) xcpretty (0.4.1) rouge (~> 3.28.0) @@ -245,4 +247,4 @@ DEPENDENCIES screengrab BUNDLED WITH - 4.0.15 + 4.0.16 diff --git a/docs/package.json b/docs/package.json index 2f913fa842a8..ee8a05710367 100644 --- a/docs/package.json +++ b/docs/package.json @@ -22,7 +22,7 @@ "katex": "^0.17.0", "react": "^19.2.7", "react-dom": "^19.2.7", - "typescript": "^7.0.2" + "typescript": "^6.0.3" }, "packageManager": "pnpm@11.11.0", "devDependencies": { diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index 5c2e916b082f..0b0e4760715d 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@astrojs/check': specifier: ^0.9.9 - version: 0.9.9(prettier@3.9.5)(typescript@7.0.2) + version: 0.9.9(prettier@3.9.5)(typescript@6.0.3) '@astrojs/markdown-satteri': specifier: ^0.3.3 version: 0.3.3 @@ -19,7 +19,7 @@ importers: version: 6.0.1(@types/node@26.1.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) '@astrojs/starlight': specifier: ^0.41.3 - version: 0.41.3(@astrojs/markdown-remark@7.2.1)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@7.0.2) + version: 0.41.3(@astrojs/markdown-remark@7.2.1)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3) '@linwooddev/style': specifier: github:LinwoodDev/style#efbdf8f05d4ef1cf6fec65c90194d30b68b3c64e&path:/packages/web version: https://codeload.github.com/LinwoodDev/style/tar.gz/efbdf8f05d4ef1cf6fec65c90194d30b68b3c64e#path:/packages/web @@ -45,8 +45,8 @@ importers: specifier: ^19.2.7 version: 19.2.7(react@19.2.7) typescript: - specifier: ^7.0.2 - version: 7.0.2 + specifier: ^6.0.3 + version: 6.0.3 devDependencies: '@vite-pwa/astro': specifier: ^1.2.0 @@ -1719,126 +1719,6 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@typescript/typescript-aix-ppc64@7.0.2': - resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} - engines: {node: '>=16.20.0'} - cpu: [ppc64] - os: [aix] - - '@typescript/typescript-darwin-arm64@7.0.2': - resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [darwin] - - '@typescript/typescript-darwin-x64@7.0.2': - resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [darwin] - - '@typescript/typescript-freebsd-arm64@7.0.2': - resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [freebsd] - - '@typescript/typescript-freebsd-x64@7.0.2': - resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [freebsd] - - '@typescript/typescript-linux-arm64@7.0.2': - resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [linux] - - '@typescript/typescript-linux-arm@7.0.2': - resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} - engines: {node: '>=16.20.0'} - cpu: [arm] - os: [linux] - - '@typescript/typescript-linux-loong64@7.0.2': - resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} - engines: {node: '>=16.20.0'} - cpu: [loong64] - os: [linux] - - '@typescript/typescript-linux-mips64el@7.0.2': - resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} - engines: {node: '>=16.20.0'} - cpu: [mips64el] - os: [linux] - - '@typescript/typescript-linux-ppc64@7.0.2': - resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} - engines: {node: '>=16.20.0'} - cpu: [ppc64] - os: [linux] - - '@typescript/typescript-linux-riscv64@7.0.2': - resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} - engines: {node: '>=16.20.0'} - cpu: [riscv64] - os: [linux] - - '@typescript/typescript-linux-s390x@7.0.2': - resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} - engines: {node: '>=16.20.0'} - cpu: [s390x] - os: [linux] - - '@typescript/typescript-linux-x64@7.0.2': - resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [linux] - - '@typescript/typescript-netbsd-arm64@7.0.2': - resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [netbsd] - - '@typescript/typescript-netbsd-x64@7.0.2': - resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [netbsd] - - '@typescript/typescript-openbsd-arm64@7.0.2': - resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [openbsd] - - '@typescript/typescript-openbsd-x64@7.0.2': - resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [openbsd] - - '@typescript/typescript-sunos-x64@7.0.2': - resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [sunos] - - '@typescript/typescript-win32-arm64@7.0.2': - resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} - engines: {node: '>=16.20.0'} - cpu: [arm64] - os: [win32] - - '@typescript/typescript-win32-x64@7.0.2': - resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} - engines: {node: '>=16.20.0'} - cpu: [x64] - os: [win32] - '@ungap/structured-clone@1.3.2': resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==} @@ -2298,8 +2178,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@2.3.0: - resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==} + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} @@ -3115,8 +2995,8 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} - nanoid@3.3.15: - resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -3252,8 +3132,8 @@ packages: resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==} engines: {node: '>=4'} - postcss@8.5.16: - resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + postcss@8.5.17: + resolution: {integrity: sha512-J7EF+8X+CzRPaJPOv9Ck2wNWJvGnnl3PcNPAdGg6GTLjyVpyQ0yATMSXRFRV01BviT/9Gwuc3rjEyJbDJG9a4w==} engines: {node: ^10 || ^12 || >=14} prettier@3.9.5: @@ -3628,8 +3508,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svgo@4.0.1: - resolution: {integrity: sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==} + svgo@4.0.2: + resolution: {integrity: sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng==} engines: {node: '>=16'} hasBin: true @@ -3699,9 +3579,9 @@ packages: typescript-auto-import-cache@0.3.6: resolution: {integrity: sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==} - typescript@7.0.2: - resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} - engines: {node: '>=16.20.0'} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} hasBin: true ufo@1.6.4: @@ -4173,12 +4053,12 @@ snapshots: jsonpointer: 5.0.1 leven: 3.1.0 - '@astrojs/check@0.9.9(prettier@3.9.5)(typescript@7.0.2)': + '@astrojs/check@0.9.9(prettier@3.9.5)(typescript@6.0.3)': dependencies: - '@astrojs/language-server': 2.16.11(prettier@3.9.5)(typescript@7.0.2) + '@astrojs/language-server': 2.16.11(prettier@3.9.5)(typescript@6.0.3) chokidar: 4.0.3 kleur: 4.1.5 - typescript: 7.0.2 + typescript: 6.0.3 yargs: 17.7.3 transitivePeerDependencies: - prettier @@ -4251,12 +4131,12 @@ snapshots: smol-toml: 1.7.0 unified: 11.0.5 - '@astrojs/language-server@2.16.11(prettier@3.9.5)(typescript@7.0.2)': + '@astrojs/language-server@2.16.11(prettier@3.9.5)(typescript@6.0.3)': dependencies: '@astrojs/compiler': 2.13.1 '@astrojs/yaml2ts': 0.2.4 '@jridgewell/sourcemap-codec': 1.5.5 - '@volar/kit': 2.4.28(typescript@7.0.2) + '@volar/kit': 2.4.28(typescript@6.0.3) '@volar/language-core': 2.4.28 '@volar/language-server': 2.4.28 '@volar/language-service': 2.4.28 @@ -4312,7 +4192,7 @@ snapshots: '@mdx-js/mdx': 3.1.1 acorn: 8.17.0 astro: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - es-module-lexer: 2.3.0 + es-module-lexer: 2.3.1 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 piccolore: 0.1.3 @@ -4363,7 +4243,7 @@ snapshots: stream-replace-string: 2.0.0 zod: 4.4.3 - '@astrojs/starlight@0.41.3(@astrojs/markdown-remark@7.2.1)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@7.0.2)': + '@astrojs/starlight@0.41.3(@astrojs/markdown-remark@7.2.1)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3)': dependencies: '@astrojs/markdown-satteri': 0.3.3 '@astrojs/mdx': 7.0.2(@astrojs/markdown-satteri@0.3.3)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) @@ -4379,7 +4259,7 @@ snapshots: hast-util-select: 6.0.4 hast-util-to-string: 3.0.1 hastscript: 9.0.1 - i18next: 26.3.6(typescript@7.0.2) + i18next: 26.3.6(typescript@6.0.3) js-yaml: 4.3.0 klona: 2.0.6 magic-string: 0.30.21 @@ -5263,8 +5143,8 @@ snapshots: hast-util-to-html: 9.0.5 hast-util-to-text: 4.0.2 hastscript: 9.0.1 - postcss: 8.5.16 - postcss-nested: 6.2.0(postcss@8.5.16) + postcss: 8.5.17 + postcss-nested: 6.2.0(postcss@8.5.17) unist-util-visit: 5.1.0 unist-util-visit-parents: 6.0.2 @@ -5853,66 +5733,6 @@ snapshots: '@types/unist@3.0.3': {} - '@typescript/typescript-aix-ppc64@7.0.2': - optional: true - - '@typescript/typescript-darwin-arm64@7.0.2': - optional: true - - '@typescript/typescript-darwin-x64@7.0.2': - optional: true - - '@typescript/typescript-freebsd-arm64@7.0.2': - optional: true - - '@typescript/typescript-freebsd-x64@7.0.2': - optional: true - - '@typescript/typescript-linux-arm64@7.0.2': - optional: true - - '@typescript/typescript-linux-arm@7.0.2': - optional: true - - '@typescript/typescript-linux-loong64@7.0.2': - optional: true - - '@typescript/typescript-linux-mips64el@7.0.2': - optional: true - - '@typescript/typescript-linux-ppc64@7.0.2': - optional: true - - '@typescript/typescript-linux-riscv64@7.0.2': - optional: true - - '@typescript/typescript-linux-s390x@7.0.2': - optional: true - - '@typescript/typescript-linux-x64@7.0.2': - optional: true - - '@typescript/typescript-netbsd-arm64@7.0.2': - optional: true - - '@typescript/typescript-netbsd-x64@7.0.2': - optional: true - - '@typescript/typescript-openbsd-arm64@7.0.2': - optional: true - - '@typescript/typescript-openbsd-x64@7.0.2': - optional: true - - '@typescript/typescript-sunos-x64@7.0.2': - optional: true - - '@typescript/typescript-win32-arm64@7.0.2': - optional: true - - '@typescript/typescript-win32-x64@7.0.2': - optional: true - '@ungap/structured-clone@1.3.2': {} '@vite-pwa/astro@1.2.0(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1))': @@ -5932,12 +5752,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@volar/kit@2.4.28(typescript@7.0.2)': + '@volar/kit@2.4.28(typescript@6.0.3)': dependencies: '@volar/language-service': 2.4.28 '@volar/typescript': 2.4.28 typesafe-path: 0.2.2 - typescript: 7.0.2 + typescript: 6.0.3 vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.1.0 @@ -6069,7 +5889,7 @@ snapshots: devalue: 5.8.1 diff: 8.0.4 dset: 3.1.4 - es-module-lexer: 2.3.0 + es-module-lexer: 2.3.1 esbuild: 0.28.1 flattie: 1.1.1 fontace: 0.4.1 @@ -6092,7 +5912,7 @@ snapshots: semver: 7.8.5 shiki: 4.3.1 smol-toml: 1.7.0 - svgo: 4.0.1 + svgo: 4.0.2 tinyclip: 0.1.15 tinyexec: 1.2.4 tinyglobby: 0.2.17 @@ -6502,7 +6322,7 @@ snapshots: es-errors@1.3.0: {} - es-module-lexer@2.3.0: {} + es-module-lexer@2.3.1: {} es-object-atoms@1.1.2: dependencies: @@ -6975,9 +6795,9 @@ snapshots: http-cache-semantics@4.2.0: {} - i18next@26.3.6(typescript@7.0.2): + i18next@26.3.6(typescript@6.0.3): optionalDependencies: - typescript: 7.0.2 + typescript: 6.0.3 idb@7.1.1: {} @@ -7732,7 +7552,7 @@ snapshots: muggle-string@0.4.1: {} - nanoid@3.3.15: {} + nanoid@3.3.16: {} neotraverse@0.6.18: {} @@ -7861,9 +7681,9 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-nested@6.2.0(postcss@8.5.16): + postcss-nested@6.2.0(postcss@8.5.17): dependencies: - postcss: 8.5.16 + postcss: 8.5.17 postcss-selector-parser: 6.1.4 postcss-selector-parser@6.1.4: @@ -7871,9 +7691,9 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss@8.5.16: + postcss@8.5.17: dependencies: - nanoid: 3.3.15 + nanoid: 3.3.16 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -8445,7 +8265,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svgo@4.0.1: + svgo@4.0.2: dependencies: commander: 11.1.0 css-select: 5.2.2 @@ -8534,28 +8354,7 @@ snapshots: dependencies: semver: 7.8.5 - typescript@7.0.2: - optionalDependencies: - '@typescript/typescript-aix-ppc64': 7.0.2 - '@typescript/typescript-darwin-arm64': 7.0.2 - '@typescript/typescript-darwin-x64': 7.0.2 - '@typescript/typescript-freebsd-arm64': 7.0.2 - '@typescript/typescript-freebsd-x64': 7.0.2 - '@typescript/typescript-linux-arm': 7.0.2 - '@typescript/typescript-linux-arm64': 7.0.2 - '@typescript/typescript-linux-loong64': 7.0.2 - '@typescript/typescript-linux-mips64el': 7.0.2 - '@typescript/typescript-linux-ppc64': 7.0.2 - '@typescript/typescript-linux-riscv64': 7.0.2 - '@typescript/typescript-linux-s390x': 7.0.2 - '@typescript/typescript-linux-x64': 7.0.2 - '@typescript/typescript-netbsd-arm64': 7.0.2 - '@typescript/typescript-netbsd-x64': 7.0.2 - '@typescript/typescript-openbsd-arm64': 7.0.2 - '@typescript/typescript-openbsd-x64': 7.0.2 - '@typescript/typescript-sunos-x64': 7.0.2 - '@typescript/typescript-win32-arm64': 7.0.2 - '@typescript/typescript-win32-x64': 7.0.2 + typescript@6.0.3: {} ufo@1.6.4: {} @@ -8706,7 +8505,7 @@ snapshots: dependencies: lightningcss: 1.32.0 picomatch: 4.0.5 - postcss: 8.5.16 + postcss: 8.5.17 rolldown: 1.1.5 tinyglobby: 0.2.17 optionalDependencies: diff --git a/tools/pubspec.lock b/tools/pubspec.lock index 4934bb9ef0cb..c6c1bc0297ca 100644 --- a/tools/pubspec.lock +++ b/tools/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: meta - sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" url: "https://pub.dev" source: hosted - version: "1.18.3" + version: "1.19.0" path: dependency: transitive description: From ef8673a418d7488f6a7153e5775873a56b795b00 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 13 Jul 2026 12:06:45 +0200 Subject: [PATCH 064/117] Fix last changelog missing onenote importer --- CHANGELOG.md | 9 +++++---- metadata/en-US/changelogs/187.txt | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98bd4a6cf8df..3643fd82e02c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,16 @@ # Changelog - - -## 2.6.0-beta.1 (2026-07-06) - + + +## 2.6.0-beta.1 (2026-07-06) + * Add pages selector with range input ([#1151](https://github.com/LinwoodDev/Butterfly/issues/1151)) * Add internal page numbers to the pages navigator ([#1143](https://github.com/LinwoodDev/Butterfly/issues/1143)) * Add cross-page area selection and deletion ([#1143](https://github.com/LinwoodDev/Butterfly/issues/1143)) * Add apply areas option to templates ([#1151](https://github.com/LinwoodDev/Butterfly/issues/1151)) * Add combine paths option ([#1071](https://github.com/LinwoodDev/Butterfly/issues/1071)) * Add xournal++ exporter +* Add OneNote importer ([#427](https://github.com/LinwoodDev/Butterfly/issues/427)) * Add next and previous page shortcuts * Improve xournal++ importer * Improve state management for better linking different systems together diff --git a/metadata/en-US/changelogs/187.txt b/metadata/en-US/changelogs/187.txt index 5a074d007ce6..c32a8a1bb808 100644 --- a/metadata/en-US/changelogs/187.txt +++ b/metadata/en-US/changelogs/187.txt @@ -4,6 +4,7 @@ * Add apply areas option to templates ([#1151](https://github.com/LinwoodDev/Butterfly/issues/1151)) * Add combine paths option ([#1071](https://github.com/LinwoodDev/Butterfly/issues/1071)) * Add xournal++ exporter +* Add OneNote importer ([#427](https://github.com/LinwoodDev/Butterfly/issues/427)) * Add next and previous page shortcuts * Improve xournal++ importer * Improve state management for better linking different systems together From 27c1f9f8fb048f4493090e773b9f805ff8460cf2 Mon Sep 17 00:00:00 2001 From: Linwood CI Date: Mon, 13 Jul 2026 12:29:10 +0000 Subject: [PATCH 065/117] Add changelog of v2.6.0-beta.2 --- CHANGELOG.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3643fd82e02c..376c61d063ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ # Changelog - + + +## 2.6.0-beta.2 (2026-07-13) + +* Add persistent document states ([#1077](https://github.com/LinwoodDev/Butterfly/issues/1077)) +* Reorder top corner menu to have home on top ([#1161](https://github.com/LinwoodDev/Butterfly/issues/1161)) +* Rebuild internal settings pages + * Add search bar to settings pages ([#1158](https://github.com/LinwoodDev/Butterfly/issues/1158)) + * Always have settings value on the right side + * Add settings descriptions +* Refactor whole state management structure ([#1157](https://github.com/LinwoodDev/Butterfly/pull/1157)) +* Remove unused view options +* Fix crash with android saf on folders with many files +* Fix blur resetting on color change +* Fix polygon collision aabb tests if closed ([#1162](https://github.com/LinwoodDev/Butterfly/pull/1162)) +* Upgrade to agb 9 + +Read more here: https://linwood.dev/butterfly/2.6.0-beta.2 ## 2.6.0-beta.1 (2026-07-06) From 9d78026321d3af143d1a580adfbb15d68a74d5a9 Mon Sep 17 00:00:00 2001 From: Linwood CI Date: Mon, 13 Jul 2026 12:40:47 +0000 Subject: [PATCH 066/117] Update Version to 2.6.0-beta.3 --- api/pubspec.yaml | 2 +- app/linux/debian/DEBIAN/control | 2 +- app/pubspec.lock | 2 +- app/pubspec.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/pubspec.yaml b/api/pubspec.yaml index 163a55e9b25e..e689d70c9dc3 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -1,6 +1,6 @@ name: butterfly_api description: The Linwood Butterfly API -version: 2.6.0-beta.2 +version: 2.6.0-beta.3 publish_to: none environment: diff --git a/app/linux/debian/DEBIAN/control b/app/linux/debian/DEBIAN/control index 4f884737660e..0b7fc5f23579 100644 --- a/app/linux/debian/DEBIAN/control +++ b/app/linux/debian/DEBIAN/control @@ -1,5 +1,5 @@ Package: linwood-butterfly -Version: 2.6.0-beta.2 +Version: 2.6.0-beta.3 Section: base Priority: optional Homepage: https://github.com/LinwoodDev/butterfly diff --git a/app/pubspec.lock b/app/pubspec.lock index dbf7a0d04c40..9db1413b2655 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -151,7 +151,7 @@ packages: path: "../api" relative: true source: path - version: "2.6.0-beta.2" + version: "2.6.0-beta.3" camera: dependency: "direct main" description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index ea59cea2d5a7..182daeecd331 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -13,7 +13,7 @@ publish_to: none # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 2.6.0-beta.2+188 +version: 2.6.0-beta.3+189 environment: sdk: ">=3.12.2 <4.0.0" From 1ae258f384b85000d556f26f76fb88450ede3cdd Mon Sep 17 00:00:00 2001 From: Linwood CI Date: Mon, 13 Jul 2026 12:46:57 +0000 Subject: [PATCH 067/117] Bump version --- app/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pubspec.yaml b/app/pubspec.yaml index b4359710a576..c1b19ad30bfc 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -13,7 +13,7 @@ publish_to: none # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 2.5.3+188 +version: 2.5.3+189 environment: sdk: ">=3.9.0 <4.0.0" From fb9bc61ae216b0e974576745bd87c1a2222ab02d Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 13 Jul 2026 20:10:56 +0200 Subject: [PATCH 068/117] Fix text labels disappearing when switching elements --- app/lib/handlers/label.dart | 47 ++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/app/lib/handlers/label.dart b/app/lib/handlers/label.dart index 32a7d044c14f..6595e3d747f0 100644 --- a/app/lib/handlers/label.dart +++ b/app/lib/handlers/label.dart @@ -4,11 +4,17 @@ class LabelHandler extends Handler with HandlerWithCursor, TextInputClient { LabelContext? _context; DocumentBloc? _bloc; + String? _editingElementId; bool _isSelecting = false; TextRange _composing = TextRange.empty; bool get isCurrentlyEditing => _context?.element != null; + @override + Map get rendererStates => { + ?_editingElementId: RendererState.hidden, + }; + LabelHandler(super.data); Future _createContext( @@ -182,20 +188,23 @@ class LabelHandler extends Handler final globalPos = context.getCameraTransform().localToGlobal(localPosition); final hitRect = _context?.getRect(); final hit = hitRect?.contains(globalPos) ?? false; - final hadFocus = focusNode.hasFocus && !hit; FocusScope.of(context.buildContext).requestFocus(focusNode); final theme = Theme.of(context.buildContext); final style = theme.textTheme.bodyLarge!; - if (hadFocus || _context?.element == null) { - if (_context?.element != null) _submit(context.getDocumentBloc()); + if (!hit || forceCreate || _context?.element == null) { + if (_context?.element != null && !hit) _submit(context.getDocumentBloc()); final utilities = currentIndex.utilities; - final hit = await context.getDocumentBloc().rayCast( - globalPos, - 0.0, - useCollection: utilities.lockCollection, - useLayer: utilities.lockLayer, - ); - final labelRenderer = hit.whereType>().firstOrNull; + final hits = forceCreate + ? >{} + : await context.getDocumentBloc().rayCast( + globalPos, + 0.0, + useCollection: utilities.lockCollection, + useLayer: utilities.lockLayer, + ); + final labelRenderer = hits + .whereType>() + .firstOrNull; if (labelRenderer == null) { _context = await _createContext( document, @@ -204,11 +213,9 @@ class LabelHandler extends Handler zoom: context.getCameraTransform().size, ); } else { - final page = context.getPage(); - if (page == null) return; final id = (labelRenderer.element as PadElement).id; if (id == null) return; - context.getDocumentBloc().add(ElementsRemoved([id])); + _editingElementId = id; _context = await _createContext( document, fileSystem, @@ -331,6 +338,7 @@ class LabelHandler extends Handler _connection = null; _context = null; _bloc = null; + _editingElementId = null; _isSelecting = false; _composing = TextRange.empty; } @@ -344,12 +352,23 @@ class LabelHandler extends Handler if (element == null) return; final id = element.id; final isEmpty = context.isEmpty; - if (context.isCreating && !isEmpty) { + final editingElementId = _editingElementId; + if (editingElementId != null && isEmpty) { + bloc.add(ElementsRemoved([editingElementId])); + bloc.delayedBake(); + } else if (editingElementId != null) { + bloc.add( + ElementsChanged({ + editingElementId: [element], + }), + ); + } else if (!isEmpty) { bloc.add(ElementsCreated([element])); } else if (!context.isCreating && isEmpty && id != null) { bloc.add(ElementsRemoved([id])); bloc.delayedBake(); } + _editingElementId = null; } @override From f8c9c90164fedeca5d38d70484e7c338833a733a Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 13 Jul 2026 20:10:56 +0200 Subject: [PATCH 069/117] Fix polygons disappearing after property changes --- app/lib/handlers/polygon.dart | 7 ++- app/test/handlers/polygon_handler_test.dart | 61 ++++++++++++++++++++- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/app/lib/handlers/polygon.dart b/app/lib/handlers/polygon.dart index 67c479801c2a..8744f2a5f4b2 100644 --- a/app/lib/handlers/polygon.dart +++ b/app/lib/handlers/polygon.dart @@ -118,8 +118,9 @@ class PolygonHandler extends Handler with ColoredHandler { final element = _element; if (element != null) { _element = element.copyWith(property: tool.property); - bloc.refreshForegrounds(); - bloc.refreshToolbar(); + unawaited(bloc.currentIndexCubit.refreshToolbar(bloc)); + unawaited(bloc.refreshForegrounds()); + return; } changeTool(bloc, tool); } @@ -381,7 +382,7 @@ class PolygonHandler extends Handler with ColoredHandler { _selectedPointIndex = null; _dragTarget = _PolygonDragTarget.newHandle; - await bloc.refreshForegrounds(); + unawaited(bloc.refreshForegrounds()); _submitElement(bloc); } diff --git a/app/test/handlers/polygon_handler_test.dart b/app/test/handlers/polygon_handler_test.dart index a632dbd27913..b63aa89e88e9 100644 --- a/app/test/handlers/polygon_handler_test.dart +++ b/app/test/handlers/polygon_handler_test.dart @@ -5,6 +5,7 @@ import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/handlers/handler.dart'; import 'package:butterfly/models/viewport.dart'; +import 'package:butterfly/renderers/renderer.dart'; import 'package:butterfly/views/toolbar/polygon.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -93,11 +94,69 @@ void main() { final toolbar = handler.getToolbar(bloc!) as PolygonToolbarView; toolbar.onToolChanged(toolbar.tool.copyWith(property: updatedProperty)); await _settleBlocEvents(); + final updatedState = bloc!.stream + .where((state) => state is DocumentLoadSuccess) + .cast() + .firstWhere( + (state) => + (state.page.content.single as PolygonElement).property == + updatedProperty, + ); (handler.getToolbar(bloc!) as PolygonToolbarView).onSubmit?.call(); - await _settleBlocEvents(); + await updatedState; final state = bloc!.state as DocumentLoadSuccess; final updatedElement = state.page.content.single as PolygonElement; expect(updatedElement.property, updatedProperty); }); + + test('toolbar changes keep edited polygon visible in foregrounds', () async { + const originalProperty = PolygonProperty( + strokeWidth: 3, + color: SRGBColor(0xFF000000), + ); + const updatedProperty = PolygonProperty( + strokeWidth: 9, + color: SRGBColor(0xFFFF0000), + ); + final element = PolygonElement( + id: 'polygon', + points: const [PolygonPoint(0, 0), PolygonPoint(10, 10)], + property: originalProperty, + ); + final page = DocumentPage( + layers: [ + DocumentLayer(id: 'layer', content: [element]), + ], + ); + final (data, pageName) = NoteData(Archive()).setPage(page, 'Page 1'); + bloc = DocumentBloc( + fileSystem, + currentIndexCubit, + windowCubit, + data, + const AssetLocation(path: 'test-note.bfly'), + null, + page, + pageName, + ); + final handler = PolygonHandler( + PolygonTool(id: 'polygon-tool', property: originalProperty), + )..editElement(element); + final toolbar = handler.getToolbar(bloc!) as PolygonToolbarView; + toolbar.onToolChanged(toolbar.tool.copyWith(property: updatedProperty)); + await _settleBlocEvents(); + + final polygon = handler + .createForegrounds( + currentIndexCubit, + (bloc!.state as DocumentLoadSuccess).data, + (bloc!.state as DocumentLoadSuccess).page, + (bloc!.state as DocumentLoadSuccess).info, + ) + .whereType() + .single + .element; + expect(polygon.property, updatedProperty); + }); } From 34ab57c301f1d0fd69243ae41bc38428e2a8c613 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 13 Jul 2026 20:10:56 +0200 Subject: [PATCH 070/117] Save renamed files before moving their references --- app/lib/views/app_bar.dart | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/app/lib/views/app_bar.dart b/app/lib/views/app_bar.dart index 19d941c65707..78fc7ac62ccb 100644 --- a/app/lib/views/app_bar.dart +++ b/app/lib/views/app_bar.dart @@ -4,7 +4,6 @@ import 'package:butterfly/actions/background.dart'; import 'package:butterfly/actions/change_path.dart'; import 'package:butterfly/actions/settings.dart'; import 'package:butterfly/actions/svg_export.dart'; -import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/api/open.dart'; import 'package:butterfly/cubits/current_index.dart'; import 'package:butterfly/cubits/transform.dart'; @@ -272,23 +271,31 @@ class _AppBarTitleState extends State<_AppBarTitle> { : _areaController.text; if (area == null || areaName == null) { final cubit = context.read(); - final fileSystem = context.read(); final location = cubit.state.location; - final documentSystem = fileSystem.buildDocumentSystem( - settings.getRemote(location.remote), - ); - if (!location.isEmpty) { - await documentSystem.deleteAsset(location.path); - await fileSystem.settingsCubit.removeRecentHistory( - location, - ); - } if (state is DocumentLoadSuccess && currentIndex.isCreating) { - await bloc.save( - location: location.copyWith(path: toFilePath(value)), + final newLocation = location.copyWith( + path: toFilePath(value), + ); + final savedLocation = await cubit.save( + bloc, + location: newLocation, force: true, ); + if (!location.isEmpty && + !savedLocation.isEmpty && + (location.path != savedLocation.path || + location.remote != savedLocation.remote)) { + final documentSystem = state.fileSystem + .buildDocumentSystem( + settings.getRemote(location.remote), + ); + await documentSystem.deleteAsset(location.path); + await cubit.state.settingsCubit.moveAssetReferences( + location, + savedLocation, + ); + } } bloc.add(DocumentDescriptionChanged(name: value)); } else { From 655276fd1c8fae473a2a85accf9bee7f00cf01e2 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 13 Jul 2026 20:10:56 +0200 Subject: [PATCH 071/117] Fix incorrect layer and page reordering --- app/lib/views/navigator/layers.dart | 9 +- app/lib/views/navigator/pages.dart | 41 ++++--- app/test/views/navigator/layers_test.dart | 132 ++++++++++++++++++++ app/test/views/navigator/pages_test.dart | 142 ++++++++++++++++++++++ 4 files changed, 309 insertions(+), 15 deletions(-) create mode 100644 app/test/views/navigator/layers_test.dart diff --git a/app/lib/views/navigator/layers.dart b/app/lib/views/navigator/layers.dart index 6921a774067f..a085d1253690 100644 --- a/app/lib/views/navigator/layers.dart +++ b/app/lib/views/navigator/layers.dart @@ -11,6 +11,9 @@ import '../../widgets/editable_list_tile.dart'; import '../../widgets/multi_select.dart'; import '../../widgets/reorderable_list_item.dart'; +int _documentLayerIndexFromViewIndex(int index, int layerCount) => + layerCount - index - 1; + class LayersView extends StatelessWidget { const LayersView({super.key}); @@ -22,6 +25,7 @@ class LayersView extends StatelessWidget { current is DocumentLoadSuccess && (previous.currentLayer != current.currentLayer || previous.invisibleLayers != current.invisibleLayers || + previous.page.layers != current.page.layers || previous.page.content != current.page.content), builder: (context, state) { if (state is! DocumentLoadSuccess) return const SizedBox.shrink(); @@ -242,7 +246,10 @@ class LayersView extends StatelessWidget { onReorderItem: (int oldIndex, int newIndex) { final layer = layers[oldIndex]; context.read().add( - LayerOrderChanged(layer.id ?? '', newIndex), + LayerOrderChanged( + layer.id ?? '', + _documentLayerIndexFromViewIndex(newIndex, layers.length), + ), ); }, ), diff --git a/app/lib/views/navigator/pages.dart b/app/lib/views/navigator/pages.dart index 0458b74899f9..1aed5db0ce1c 100644 --- a/app/lib/views/navigator/pages.dart +++ b/app/lib/views/navigator/pages.dart @@ -187,25 +187,38 @@ class _PagesViewState extends State { itemCount: all.length, onReorderItem: (oldIndex, newIndex) { if (oldIndex < 0 || + oldIndex >= all.length || newIndex < 0 || - oldIndex >= all.length) { + newIndex >= all.length || + !all[oldIndex].isFile) { return; } - final current = all[oldIndex]; - final name = current.path; - final isFile = current.isFile; - if (!isFile) return; - final next = - all[newIndex.clamp(0, all.length - 1)]; - var nextIndex = state.data.getPageIndex( - next.path, - ); - if (newIndex >= all.length && nextIndex != null) { - nextIndex++; + final anchorIndex = newIndex > oldIndex + ? newIndex + 1 + : newIndex; + int? pageIndex; + if (anchorIndex < all.length) { + final anchor = all[anchorIndex]; + if (!anchor.isFile) return; + pageIndex = state.data.getPageIndex( + anchor.path, + ); + } else { + pageIndex = + state.data + .getPages(true) + .map(state.data.getPageIndex) + .nonNulls + .fold( + -1, + (maximum, index) => + index > maximum ? index : maximum, + ) + + 1; } - if (!next.isFile || nextIndex == null) return; + if (pageIndex == null) return; context.read().add( - PageReordered(name, nextIndex), + PageReordered(all[oldIndex].path, pageIndex), ); }, itemBuilder: (BuildContext context, int index) { diff --git a/app/test/views/navigator/layers_test.dart b/app/test/views/navigator/layers_test.dart new file mode 100644 index 000000000000..5765932c0c6e --- /dev/null +++ b/app/test/views/navigator/layers_test.dart @@ -0,0 +1,132 @@ +import 'package:archive/archive.dart'; +import 'package:butterfly/bloc/document_bloc.dart'; +import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/settings.dart'; +import 'package:butterfly/cubits/transform.dart'; +import 'package:butterfly/models/viewport.dart'; +import 'package:butterfly/src/generated/i18n/app_localizations.dart'; +import 'package:butterfly/views/navigator/layers.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lw_file_system/lw_file_system.dart'; +import 'package:material_leap/material_leap.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../helpers/mocks.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('layer operations stay consistent across UI and document state', ( + tester, + ) async { + final fileSystem = MockButterflyFileSystem(); + final settingsCubit = fileSystem.settingsCubit as MockSettingsCubit; + when( + () => settingsCubit.state, + ).thenReturn(const ButterflySettings(autosave: false)); + when(() => settingsCubit.stream).thenAnswer((_) => const Stream.empty()); + + final currentIndexCubit = CurrentIndexCubit( + settingsCubit, + TransformCubit(1), + CameraViewport.unbaked(), + ); + final windowCubit = WindowCubit(fullScreen: false); + const page = DocumentPage( + layers: [ + DocumentLayer(id: 'bottom', name: 'Bottom'), + DocumentLayer(id: 'middle', name: 'Middle'), + DocumentLayer(id: 'top', name: 'Top'), + ], + ); + final (data, pageName) = NoteData(Archive()).setPage(page, 'Page'); + final bloc = DocumentBloc( + fileSystem, + currentIndexCubit, + windowCubit, + data, + const AssetLocation(path: 'layers-test.bfly'), + null, + page, + pageName, + ); + addTearDown(() async { + await bloc.close(); + await currentIndexCubit.close(); + await windowCubit.close(); + }); + + await tester.pumpWidget( + BlocProvider.value( + value: bloc, + child: MaterialApp( + locale: const Locale('en'), + localizationsDelegates: const [ + ...AppLocalizations.localizationsDelegates, + LeapLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: const Scaffold(body: SizedBox(width: 500, child: LayersView())), + ), + ), + ); + await tester.pumpAndSettle(); + + double verticalPosition(String id) => + tester.getTopLeft(find.byKey(ValueKey(id))).dy; + + expect(verticalPosition('top'), lessThan(verticalPosition('middle'))); + expect(verticalPosition('middle'), lessThan(verticalPosition('bottom'))); + + final topHandle = find.descendant( + of: find.byKey(const ValueKey('top')), + matching: find.byType(ReorderableDragStartListener), + ); + await tester.timedDrag( + topHandle, + const Offset(0, 240), + const Duration(seconds: 1), + ); + await tester.pumpAndSettle(); + + var state = bloc.state as DocumentLoadSuccess; + expect(state.page.layers.map((layer) => layer.id), [ + 'top', + 'bottom', + 'middle', + ]); + expect(verticalPosition('middle'), lessThan(verticalPosition('bottom'))); + expect(verticalPosition('bottom'), lessThan(verticalPosition('top'))); + + bloc + ..add(const LayerChanged('top', name: 'Renamed')) + ..add(const LayerVisibilityChanged('bottom', false)) + ..add(const LayerCreated(id: 'new', name: 'New')); + await tester.pumpAndSettle(); + + state = bloc.state as DocumentLoadSuccess; + expect(state.page.getLayer('top').name, 'Renamed'); + expect(state.isLayerVisible('bottom'), isFalse); + expect(state.page.layers.map((layer) => layer.id), [ + 'top', + 'bottom', + 'middle', + 'new', + ]); + expect(verticalPosition('new'), lessThan(verticalPosition('middle'))); + + bloc.add(const LayerRemoved('middle')); + await tester.pumpAndSettle(); + + state = bloc.state as DocumentLoadSuccess; + expect(state.page.layers.map((layer) => layer.id), [ + 'top', + 'bottom', + 'new', + ]); + expect(find.byKey(const ValueKey('middle')), findsNothing); + }); +} diff --git a/app/test/views/navigator/pages_test.dart b/app/test/views/navigator/pages_test.dart index 61efb9e6e829..9e4bff214613 100644 --- a/app/test/views/navigator/pages_test.dart +++ b/app/test/views/navigator/pages_test.dart @@ -1,7 +1,24 @@ +import 'package:archive/archive.dart'; +import 'package:butterfly/bloc/document_bloc.dart'; +import 'package:butterfly/cubits/current_index.dart'; +import 'package:butterfly/cubits/settings.dart'; +import 'package:butterfly/cubits/transform.dart'; +import 'package:butterfly/models/viewport.dart'; +import 'package:butterfly/src/generated/i18n/app_localizations.dart'; import 'package:butterfly/views/navigator/pages.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:lw_file_system/lw_file_system.dart'; +import 'package:material_leap/material_leap.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../helpers/mocks.dart'; void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + group('page navigator path helpers', () { test('shows root untitled page', () { final pages = [('', '0.')]; @@ -57,4 +74,129 @@ void main() { ); }); }); + + testWidgets('dragging pages updates document and visible order', ( + tester, + ) async { + final fileSystem = MockButterflyFileSystem(); + final settingsCubit = fileSystem.settingsCubit as MockSettingsCubit; + when( + () => settingsCubit.state, + ).thenReturn(const ButterflySettings(autosave: false)); + when(() => settingsCubit.stream).thenAnswer((_) => const Stream.empty()); + final currentIndexCubit = CurrentIndexCubit( + settingsCubit, + TransformCubit(1), + CameraViewport.unbaked(), + ); + final windowCubit = WindowCubit(fullScreen: false); + + var data = NoteData(Archive()); + late String firstPath; + late String thirdPath; + (data, firstPath) = data.setPage( + const DocumentPage(layers: [DocumentLayer(id: 'first-layer')]), + 'First', + ); + (data, _) = data.setPage( + const DocumentPage(layers: [DocumentLayer(id: 'second-layer')]), + 'Second', + ); + (data, _) = data.setPage( + const DocumentPage(layers: [DocumentLayer(id: 'third-layer')]), + 'Third', + ); + (data, _) = data.setPage( + const DocumentPage(layers: [DocumentLayer(id: 'fourth-layer')]), + 'Fourth', + ); + (data, thirdPath) = data.setPage( + const DocumentPage(layers: [DocumentLayer(id: 'fifth-layer')]), + 'Fifth', + ); + final page = data.getPage(thirdPath)!; + final bloc = DocumentBloc( + fileSystem, + currentIndexCubit, + windowCubit, + data, + const AssetLocation(path: 'pages-test.bfly'), + null, + page, + thirdPath, + ); + addTearDown(() async { + await bloc.close(); + await currentIndexCubit.close(); + await windowCubit.close(); + }); + + await tester.pumpWidget( + BlocProvider.value( + value: bloc, + child: MaterialApp( + locale: const Locale('en'), + localizationsDelegates: const [ + ...AppLocalizations.localizationsDelegates, + LeapLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: const Scaffold(body: SizedBox(width: 500, child: PagesView())), + ), + ), + ); + await tester.pumpAndSettle(); + + double verticalPosition(String path) => + tester.getTopLeft(find.byKey(ValueKey(path)).first).dy; + final firstHandle = find.descendant( + of: find.byKey(ValueKey(firstPath)), + matching: find.byType(ReorderableDragStartListener), + ); + final gesture = await tester.startGesture(tester.getCenter(firstHandle)); + await gesture.moveTo( + tester.getRect(find.byKey(ValueKey(thirdPath)).first).bottomCenter - + const Offset(0, 1), + timeStamp: const Duration(seconds: 1), + ); + await gesture.up(); + await tester.pumpAndSettle(); + + final state = bloc.state as DocumentLoadSuccess; + expect(state.data.getPages(), [ + 'Second', + 'Third', + 'Fourth', + 'First', + 'Fifth', + ]); + final paths = state.data.getPages(true); + expect(verticalPosition(paths[0]), lessThan(verticalPosition(paths[1]))); + expect(verticalPosition(paths[1]), lessThan(verticalPosition(paths[2]))); + expect(verticalPosition(paths[2]), lessThan(verticalPosition(paths[3]))); + }); + + test('page model accepts converted Flutter final indices', () { + var data = NoteData(Archive()); + late String firstPath; + (data, firstPath) = data.setPage(const DocumentPage(), 'First'); + (data, _) = data.setPage(const DocumentPage(), 'Second'); + (data, _) = data.setPage(const DocumentPage(), 'Third'); + (data, _) = data.setPage(const DocumentPage(), 'Fourth'); + (data, _) = data.setPage(const DocumentPage(), 'Fifth'); + + data = data.reorderPage(firstPath, 3); + expect(data.getPages(), ['Second', 'Third', 'First', 'Fourth', 'Fifth']); + + final movedFirstPath = data.getPages(true)[2]; + final endIndex = + data + .getPages(true) + .map(data.getPageIndex) + .nonNulls + .fold(-1, (maximum, index) => index > maximum ? index : maximum) + + 1; + data = data.reorderPage(movedFirstPath, endIndex); + expect(data.getPages(), ['Second', 'Third', 'Fourth', 'Fifth', 'First']); + }); } From 7c0f410c830917d6980da9e607a58c6d95e7c8c6 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 13 Jul 2026 20:10:56 +0200 Subject: [PATCH 072/117] Support spacer hit testing across element types --- app/lib/renderers/elements/shape.dart | 23 ++++++-- app/lib/renderers/renderer.dart | 30 ++++++++++ app/test/renderers/shape_renderer_test.dart | 63 +++++++++++++++++++++ 3 files changed, 112 insertions(+), 4 deletions(-) diff --git a/app/lib/renderers/elements/shape.dart b/app/lib/renderers/elements/shape.dart index 7c65319ae7bf..cc286a3bd7d8 100644 --- a/app/lib/renderers/elements/shape.dart +++ b/app/lib/renderers/elements/shape.dart @@ -347,6 +347,16 @@ class ShapeHitCalculator extends HitCalculator { final center = this.rect.center; bool hitCircle() { + if (!isFiniteRect(rect)) { + return full + ? [ + this.rect.topLeft, + this.rect.topRight, + this.rect.bottomRight, + this.rect.bottomLeft, + ].every(rect.contains) + : boundsRect.overlaps(rect); + } final circleCenter = this.rect.center; final rectCenter = rect.center; final dx = (circleCenter.dx - rectCenter.dx).abs(); @@ -369,6 +379,14 @@ class ShapeHitCalculator extends HitCalculator { } bool hitRect() { + if (!isFiniteRect(rect)) { + return [ + this.rect.topLeft, + this.rect.topRight, + this.rect.bottomRight, + this.rect.bottomLeft, + ].any(rect.contains); + } final topLeft = rect.topLeft.rotate(center, rotation); final topRight = rect.topRight.rotate(center, rotation); final bottomLeft = rect.bottomLeft.rotate(center, rotation); @@ -443,10 +461,7 @@ class ShapeHitCalculator extends HitCalculator { ], this.rect.bottomRight); return isTopCenter && isBottomLeft && isBottomRight; } - return isPolygonInPolygon( - [rect.topLeft, rect.topRight, rect.bottomRight, rect.bottomLeft], - [topCenter, bottomLeft, bottomRight], - ); + return hitRectPolygon(rect, [topCenter, bottomLeft, bottomRight]); } return switch (shape) { diff --git a/app/lib/renderers/renderer.dart b/app/lib/renderers/renderer.dart index 374fbff5744c..89fa33b7fe6d 100644 --- a/app/lib/renderers/renderer.dart +++ b/app/lib/renderers/renderer.dart @@ -76,6 +76,9 @@ class DefaultHitCalculator extends HitCalculator { if (full) { return rotated.every(rect.contains); } + if (!isFiniteRect(rect)) { + return rotated.any(rect.contains); + } return isPolygonInPolygon(rotated, [ rect.topLeft, rect.topRight, @@ -136,6 +139,7 @@ abstract class HitCalculator { bool hitPolygon(List polygon, {bool full = false}); bool isPointInPolygon(List polygon, Offset testPoint) { + if (!_isFiniteOffset(testPoint) || !isFinitePolygon(polygon)) return false; bool result = false; int j = polygon.length - 1; for (int i = 0; i < polygon.length; i++) { @@ -154,6 +158,31 @@ abstract class HitCalculator { return result; } + bool _isFiniteOffset(Offset point) => point.dx.isFinite && point.dy.isFinite; + + bool isFinitePolygon(List polygon) => polygon.every(_isFiniteOffset); + + bool isFiniteRect(Rect rect) => + rect.left.isFinite && + rect.top.isFinite && + rect.right.isFinite && + rect.bottom.isFinite; + + List rectToPolygon(Rect rect) => [ + rect.topLeft, + rect.topRight, + rect.bottomRight, + rect.bottomLeft, + ]; + + bool hitRectPolygon(Rect rect, List polygon) { + if (polygon.isEmpty) return false; + if (!isFiniteRect(rect)) { + return polygon.any(rect.contains); + } + return isPolygonInPolygon(rectToPolygon(rect), polygon); + } + List getAxesOfPolygon(List polygon) { List axes = []; for (int i = 0; i < polygon.length; i++) { @@ -221,6 +250,7 @@ abstract class HitCalculator { bool isPolygonInPolygon(List poly1, List poly2) { if (poly1.isEmpty || poly2.isEmpty) return false; + if (!isFinitePolygon(poly1) || !isFinitePolygon(poly2)) return false; for (final (a, b) in _edgesOf(poly1)) { for (final (c, d) in _edgesOf(poly2)) { diff --git a/app/test/renderers/shape_renderer_test.dart b/app/test/renderers/shape_renderer_test.dart index 0d2e2ec89925..0a218cefa9dc 100644 --- a/app/test/renderers/shape_renderer_test.dart +++ b/app/test/renderers/shape_renderer_test.dart @@ -95,4 +95,67 @@ void main() { expect(hitCalculator.hit(const Rect.fromLTWH(50, 49, 2, 2)), isTrue); }); + + group('unbounded spacer rectangles', () { + const rightSpacerRect = Rect.fromLTRB( + 50, + -double.infinity, + double.infinity, + double.infinity, + ); + const leftSpacerRect = Rect.fromLTRB( + -double.infinity, + -double.infinity, + 50, + double.infinity, + ); + + test('default renderers', () { + final calculator = DefaultHitCalculator( + const Rect.fromLTWH(100, 100, 40, 40), + const Rect.fromLTWH(100, 100, 40, 40), + 0, + ); + + expect(calculator.hit(rightSpacerRect), isTrue); + expect(calculator.hit(leftSpacerRect), isFalse); + }); + + for (final entry in { + 'circle': CircleShape(), + 'rectangle': RectangleShape(), + 'triangle': TriangleShape(), + 'line': LineShape(), + }.entries) { + test('${entry.key} shapes', () { + final calculator = ShapeRenderer( + ShapeElement( + firstPosition: const Point(100, 100), + secondPosition: const Point(140, 140), + property: ShapeProperty(shape: entry.value, strokeWidth: 2), + ), + ).getHitCalculator(); + + expect(calculator.hit(rightSpacerRect), isTrue); + expect(calculator.hit(leftSpacerRect), isFalse); + }); + } + + test('polygons', () { + final calculator = PolygonHitCalculator( + const Rect.fromLTWH(100, 100, 40, 40), + const [ + PolygonPoint(100, 100), + PolygonPoint(140, 100), + PolygonPoint(140, 140), + PolygonPoint(100, 140), + ], + 0, + const PolygonProperty(strokeWidth: 2), + ); + + expect(calculator.hit(rightSpacerRect), isTrue); + expect(calculator.hit(leftSpacerRect), isFalse); + }); + }); } From dad43945343ef02ff09748719f688dba096f4de0 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 13 Jul 2026 20:10:56 +0200 Subject: [PATCH 073/117] Allow zoom controls while zoom is locked --- app/lib/views/zoom.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/views/zoom.dart b/app/lib/views/zoom.dart index 47820adc028e..539dc30ea6e5 100644 --- a/app/lib/views/zoom.dart +++ b/app/lib/views/zoom.dart @@ -73,7 +73,7 @@ class _ZoomViewState extends State with TickerProviderStateMixin { } final size = currentIndex.cameraViewport.toRealSize(); final center = Offset(size.width / 2, size.height / 2); - currentIndexCubit.size(value, center); + currentIndexCubit.size(value, center, true); if (bake) { currentIndexCubit.bake(documentState); } From 4cffcfbe34cfecb39df990c80cacf674acc2b871 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 13 Jul 2026 20:10:56 +0200 Subject: [PATCH 074/117] Respect hidden extensions in recent files --- app/lib/views/files/recent.dart | 24 +++++- app/test/views/home/recent_files_test.dart | 85 ++++++++++++++++++++++ 2 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 app/test/views/home/recent_files_test.dart diff --git a/app/lib/views/files/recent.dart b/app/lib/views/files/recent.dart index 03a0a651950d..3d103ca954da 100644 --- a/app/lib/views/files/recent.dart +++ b/app/lib/views/files/recent.dart @@ -49,19 +49,20 @@ class RecentFilesViewState extends State { ); Widget _getItem(FileSystemEntity entity) { + final settings = context.read().state; FileMetadata? metadata; Uint8List? thumbnail; if (entity is FileSystemFile) { final data = entity.data?.display(); metadata = data?.getMetadata(); - if (context.read().state.showThumbnails) { + if (settings.showThumbnails) { thumbnail = data?.getThumbnail(); } } return AssetCard( metadata: metadata, thumbnail: thumbnail, - name: entity.location.pathWithoutLeadingSlash, + name: _getDisplayName(entity.location, settings.hideExtension), tooltip: entity.identifier, height: double.infinity, onTap: () => widget.onFileTap == null @@ -77,7 +78,9 @@ class RecentFilesViewState extends State { @override Widget build(BuildContext context) { return BlocListener( - listenWhen: (previous, current) => previous.history != current.history, + listenWhen: (previous, current) => + previous.history != current.history || + previous.hideExtension != current.hideExtension, listener: (_, state) => reload(state), child: StreamBuilder>>( stream: _stream, @@ -113,4 +116,19 @@ class RecentFilesViewState extends State { ), ); } + + String _getDisplayName(AssetLocation location, bool hideExtension) { + final path = location.pathWithoutLeadingSlash; + if (!hideExtension) return path; + + final fileName = location.fileName; + final fileNameWithoutExtension = location.fileNameWithoutExtension; + if (fileName == fileNameWithoutExtension) return path; + + return path.replaceRange( + path.length - fileName.length, + path.length, + fileNameWithoutExtension, + ); + } } diff --git a/app/test/views/home/recent_files_test.dart b/app/test/views/home/recent_files_test.dart new file mode 100644 index 000000000000..cb3408bb8804 --- /dev/null +++ b/app/test/views/home/recent_files_test.dart @@ -0,0 +1,85 @@ +import 'dart:async'; + +import 'package:archive/archive.dart'; +import 'package:butterfly/api/file_system.dart'; +import 'package:butterfly/cubits/settings.dart'; +import 'package:butterfly/views/files/recent.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lw_file_system/lw_file_system.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../helpers/mocks.dart'; + +void main() { + late MockSettingsCubit settingsCubit; + late MockButterflyFileSystem butterflyFileSystem; + late StreamController settingsController; + late ButterflySettings settings; + + setUp(() async { + settings = const ButterflySettings( + history: [AssetLocation(path: '/folder/note.bfly')], + hideExtension: true, + showThumbnails: false, + ); + settingsController = StreamController.broadcast(); + settingsCubit = MockSettingsCubit(); + butterflyFileSystem = MockButterflyFileSystem(settingsCubit: settingsCubit); + + when(() => settingsCubit.state).thenAnswer((_) => settings); + when( + () => settingsCubit.stream, + ).thenAnswer((_) => settingsController.stream); + when(() => settingsCubit.getRemote(any())).thenReturn(null); + + await butterflyFileSystem.buildDocumentSystem().updateFile( + '/folder/note.bfly', + NoteData(Archive()).toFile(), + ); + }); + + tearDown(() async { + await settingsController.close(); + }); + + Widget createWidgetUnderTest() { + return MultiRepositoryProvider( + providers: [ + RepositoryProvider.value( + value: butterflyFileSystem, + ), + ], + child: MaterialApp( + home: BlocProvider.value( + value: settingsCubit, + child: const RecentFilesView(replace: false), + ), + ), + ); + } + + testWidgets('hides file extension in recent file path', (tester) async { + await tester.pumpWidget(createWidgetUnderTest()); + await tester.pumpAndSettle(); + + expect(find.text('folder/note'), findsOneWidget); + expect(find.text('folder/note.bfly'), findsNothing); + }); + + testWidgets('updates recent file path when hide extension changes', ( + tester, + ) async { + await tester.pumpWidget(createWidgetUnderTest()); + await tester.pumpAndSettle(); + + settings = settings.copyWith(hideExtension: false); + settingsController.add(settings); + await tester.pumpAndSettle(); + + expect(find.text('folder/note.bfly'), findsOneWidget); + expect(find.text('folder/note'), findsNothing); + }); +} From 13bd109f9a4b8f8a95a5458ddbfb6e474d6070b6 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 13 Jul 2026 20:10:56 +0200 Subject: [PATCH 075/117] Persist the thumbnail visibility setting --- app/lib/cubits/settings.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/app/lib/cubits/settings.dart b/app/lib/cubits/settings.dart index 081f2277a9b3..dab6ea68ff8f 100644 --- a/app/lib/cubits/settings.dart +++ b/app/lib/cubits/settings.dart @@ -842,6 +842,7 @@ sealed class ButterflySettings with _$ButterflySettings, LeapSettings { } await prefs.setBool('show_verbose_logs', showVerboseLogs); await prefs.setBool('hide_extension', hideExtension); + await prefs.setBool('show_thumbnails', showThumbnails); } ExternalStorage? getRemote(String? identifier) { From 0929257e442d98e4053729587b0bd596430131c8 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 13 Jul 2026 20:10:56 +0200 Subject: [PATCH 076/117] Correct the texture height label --- app/lib/dialogs/texture.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/dialogs/texture.dart b/app/lib/dialogs/texture.dart index 06b53c02c79c..28bb791d55fa 100644 --- a/app/lib/dialogs/texture.dart +++ b/app/lib/dialogs/texture.dart @@ -158,7 +158,7 @@ class _TextureViewState extends State { ExactSlider( onChanged: (value) => widget.onChanged(widget.value.copyWith(boxHeight: value)), - header: Text(AppLocalizations.of(context).width), + header: Text(AppLocalizations.of(context).height), value: widget.value.boxHeight, defaultValue: 0, min: 0, From 0f851727cb4f8ed343ecfd84a6189e142caab916 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 13 Jul 2026 20:10:56 +0200 Subject: [PATCH 077/117] Prevent repeated saves and mark imports unsaved --- app/lib/cubits/current_index.dart | 7 ++++++- app/lib/views/main.dart | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/lib/cubits/current_index.dart b/app/lib/cubits/current_index.dart index fc86424841c2..c2a5638df367 100644 --- a/app/lib/cubits/current_index.dart +++ b/app/lib/cubits/current_index.dart @@ -2417,7 +2417,7 @@ class CurrentIndexCubit extends Cubit { bool isAutosave = false, }) async { final absolute = state.absolute; - if (!force && + if (location == null && (state.saved == SaveState.saved || state.saved == SaveState.absoluteRead)) { return state.location; @@ -2440,6 +2440,11 @@ class CurrentIndexCubit extends Cubit { } } return _savingLock.synchronized(() async { + if (location == null && + (state.saved == SaveState.saved || + state.saved == SaveState.absoluteRead)) { + return state.location; + } var current = location ?? state.location; if (isClosed) { return current; diff --git a/app/lib/views/main.dart b/app/lib/views/main.dart index 4af6c6e42b7f..fb3d795ad75b 100644 --- a/app/lib/views/main.dart +++ b/app/lib/views/main.dart @@ -357,6 +357,11 @@ class _ProjectPageState extends State { page, pageName, ); + final isImportedDocument = + documentOpened && !(location.fileType?.isNote() ?? false); + if (!absolute && isImportedDocument) { + _currentIndexCubit!.setSaveState(saved: SaveState.unsaved); + } networkingService.setup(_bloc!); _importService = ImportService(context, bloc: _bloc); _exportService = ExportService(context, _bloc); From 78f05fcfdb9fa633d44348c5b5a3f4b07567311c Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 13 Jul 2026 20:10:56 +0200 Subject: [PATCH 078/117] Fix WebDAV connectivity and SAF filesystem selection --- api/pubspec.yaml | 2 +- app/lib/settings/connections.dart | 51 +++++++++++++++++-------------- app/pubspec.yaml | 2 +- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/api/pubspec.yaml b/api/pubspec.yaml index 19d8c95b1c3a..ad7ae3cc1ebf 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: git: url: https://github.com/LinwoodDev/dart_pkgs.git path: packages/lw_file_system_api - ref: 5fd8a282404929e54c36a25fd80e43ca3a85bc46 + ref: 37594211585889e35e65dad382d4fdcf29ac69a0 dart_leap: git: url: https://github.com/LinwoodDev/dart_pkgs.git diff --git a/app/lib/settings/connections.dart b/app/lib/settings/connections.dart index ee42b6e605ae..f88aa6b65a52 100644 --- a/app/lib/settings/connections.dart +++ b/app/lib/settings/connections.dart @@ -257,14 +257,15 @@ class __AddRemoteDialogState extends State<_AddRemoteDialog> { _showCreatingError(loc.urlNotValid); return; } - final request = await _httpClient.getUrl(url); - request.headers.add('Accept', 'application/json'); - request.headers.add( - 'Authorization', - 'Basic ${base64Encode(utf8.encode('${_usernameController.text}:${_passwordController.text}'))}', + final storage = _buildDavRemoteStorage(url: url.toString()); + final passwordStorage = InMemoryPasswordStorage() + ..write(storage, _passwordController.text); + final isConnected = await DavRemoteDirectoryFileSystem.checkConnectivity( + storage: storage, + passwordStorage: passwordStorage, + certificateSha1: _certificateSha1, ); - final response = await request.close(); - if (response.statusCode != 200) { + if (!isConnected) { _showCreatingError(loc.cannotConnect); return; } @@ -388,22 +389,7 @@ class __AddRemoteDialogState extends State<_AddRemoteDialog> { final settingsCubit = context.read(); final icon = await _getIcon(); final remoteStorage = switch (widget.storage) { - DavRemoteStorage() => DavRemoteStorage( - name: _nameController.text, - username: _usernameController.text, - url: _urlController.text, - paths: { - '': _directoryController.text, - 'documents': _documentsDirectoryController.text, - 'templates': _templatesDirectoryController.text, - 'packs': _packsDirectoryController.text, - }, - certificateSha1: _certificateSha1, - icon: icon, - pinnedPaths: { - 'documents': [if (_syncRootDirectory) '/'], - }, - ), + DavRemoteStorage() => _buildDavRemoteStorage(icon: icon), LocalStorage() => LocalStorage( name: _nameController.text, paths: { @@ -422,6 +408,25 @@ class __AddRemoteDialogState extends State<_AddRemoteDialog> { navigator.pop(); } + DavRemoteStorage _buildDavRemoteStorage({String? url, Uint8List? icon}) { + return DavRemoteStorage( + name: _nameController.text, + username: _usernameController.text, + url: url ?? _urlController.text, + paths: { + '': _directoryController.text, + 'documents': _documentsDirectoryController.text, + 'templates': _templatesDirectoryController.text, + 'packs': _packsDirectoryController.text, + }, + certificateSha1: _certificateSha1, + icon: icon, + pinnedPaths: { + 'documents': [if (_syncRootDirectory) '/'], + }, + ); + } + Future _showCreatingError(String error, [dynamic e]) { return showDialog( context: context, diff --git a/app/pubspec.yaml b/app/pubspec.yaml index c1b19ad30bfc..9aab71c9ad6f 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -62,7 +62,7 @@ dependencies: material_leap: git: url: https://github.com/LinwoodDev/dart_pkgs.git - ref: b12a98c7236116c39ed8f3f2bf7f7666163d4fdf + ref: 064aed61c361194bf0eb3cb599e62bdd3c111d7c path: packages/material_leap lw_sysapi: git: From ddf4a5cd7e04d69fd62a0d4660651e77e9fa098d Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 13 Jul 2026 20:10:56 +0200 Subject: [PATCH 079/117] Update Flutter and project dependencies --- api/pubspec.lock | 28 +-- .../Flutter/GeneratedPluginRegistrant.swift | 2 + app/pubspec.lock | 182 +++++++++--------- app/pubspec.yaml | 4 +- tools/pubspec.lock | 10 +- 5 files changed, 110 insertions(+), 116 deletions(-) diff --git a/api/pubspec.lock b/api/pubspec.lock index bdd23b988120..ed4d48100608 100644 --- a/api/pubspec.lock +++ b/api/pubspec.lock @@ -61,34 +61,34 @@ packages: dependency: transitive description: name: build - sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10 + sha256: "45d14a0fb23e018d8287c32fc98d726ce466b231928ed9b9200f29bd3ccd39ae" url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "4.0.7" build_config: dependency: transitive description: name: build_config - sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" + sha256: f2c223156a26eea323e6244b85141d76413a80aeee9fe0b380773789fabaf8ae url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" build_daemon: dependency: transitive description: name: build_daemon - sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + sha256: fd754058c342243718d5171a95f352cfc9fcf0cba8cfa26df67cb13a5836db78 url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6" + sha256: "5367e521935b102bdf1e735d2aab461e36b2edca6517662d088dd04cc39f8d16" url: "https://pub.dev" source: hosted - version: "2.15.0" + version: "2.15.1" built_collection: dependency: transitive description: @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: coverage - sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.15.1" crypto: dependency: transitive description: @@ -302,8 +302,8 @@ packages: dependency: "direct main" description: path: "packages/lw_file_system_api" - ref: "5fd8a282404929e54c36a25fd80e43ca3a85bc46" - resolved-ref: "5fd8a282404929e54c36a25fd80e43ca3a85bc46" + ref: "37594211585889e35e65dad382d4fdcf29ac69a0" + resolved-ref: "37594211585889e35e65dad382d4fdcf29ac69a0" url: "https://github.com/LinwoodDev/dart_pkgs.git" source: git version: "1.0.0" @@ -319,10 +319,10 @@ packages: dependency: transitive description: name: meta - sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" url: "https://pub.dev" source: hosted - version: "1.18.3" + version: "1.19.0" mime: dependency: transitive description: diff --git a/app/macos/Flutter/GeneratedPluginRegistrant.swift b/app/macos/Flutter/GeneratedPluginRegistrant.swift index 71ca483802e8..1bda1a93629f 100644 --- a/app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -14,6 +14,7 @@ import flutter_secure_storage_darwin import irondash_engine_context import network_info_plus import package_info_plus +import pdfium_flutter import screen_retriever_macos import share_plus import shared_preferences_foundation @@ -31,6 +32,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PDFiumFlutterPlugin.register(with: registry.registrar(forPlugin: "PDFiumFlutterPlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/app/pubspec.lock b/app/pubspec.lock index b73ba8ddd110..ec09402407c9 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -93,34 +93,34 @@ packages: dependency: transitive description: name: build - sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10 + sha256: "45d14a0fb23e018d8287c32fc98d726ce466b231928ed9b9200f29bd3ccd39ae" url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "4.0.7" build_config: dependency: transitive description: name: build_config - sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71" + sha256: f2c223156a26eea323e6244b85141d76413a80aeee9fe0b380773789fabaf8ae url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" build_daemon: dependency: transitive description: name: build_daemon - sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + sha256: fd754058c342243718d5171a95f352cfc9fcf0cba8cfa26df67cb13a5836db78 url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6" + sha256: "5367e521935b102bdf1e735d2aab461e36b2edca6517662d088dd04cc39f8d16" url: "https://pub.dev" source: hosted - version: "2.15.0" + version: "2.15.1" built_collection: dependency: transitive description: @@ -143,7 +143,7 @@ packages: path: "../api" relative: true source: path - version: "2.5.3" + version: "2.5.4" camera: dependency: "direct main" description: @@ -156,34 +156,34 @@ packages: dependency: transitive description: name: camera_android_camerax - sha256: e20c1e92ce6797d9ae9b1db1e09a4c1039a04827d0b24985f5da3840b96948ac + sha256: cf6c248ef3d3c4846e99e488de9487d95927437e083c396e5fed2c1ece7f93a7 url: "https://pub.dev" source: hosted - version: "0.7.2+1" + version: "0.7.4+1" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "90e4cc3fde331581a3b2d35d83be41dbb7393af0ab857eb27b732174289cb96d" + sha256: "866e9cd8370f8055d005c0413937a52dc1d7a472687f0ee3ce02392955aababa" url: "https://pub.dev" source: hosted - version: "0.10.1" + version: "0.10.2" camera_platform_interface: dependency: transitive description: name: camera_platform_interface - sha256: "7ac852d77699acee79f0d438b793feee26721841e50973576419ff5c6d95e9b7" + sha256: "4524ca6eb4176b066864036ad4fe02c3e4863e63b77eadc21a5bf56824f43498" url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.13.1" camera_web: dependency: transitive description: name: camera_web - sha256: "57f49a635c8bf249d07fb95eb693d7e4dda6796dedb3777f9127fb54847beba7" + sha256: "1245a480a113437f8d46d19c0fb90cea9db921436d9cf2ba5fb11854a1312693" url: "https://pub.dev" source: hosted - version: "0.3.5+3" + version: "0.3.5+4" camera_windows: dependency: "direct main" description: @@ -212,10 +212,10 @@ packages: dependency: transitive description: name: cli_util - sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + sha256: "5909d2c6b66817222779e1eedc19e0e28b76d1df7bd9856a4792ccb9881df358" url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "0.5.1" clock: dependency: transitive description: @@ -228,10 +228,10 @@ packages: dependency: transitive description: name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.2.1" collection: dependency: "direct main" description: @@ -244,10 +244,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "62ffa266d9a23b79fb3fcbc206afc00bb979417ba57b1324c546b5aab95ba057" + sha256: cad0e811a289ea2a941119dc483c204ec1684cbb9a8fc7351fe4a230b8313160 url: "https://pub.dev" source: hosted - version: "7.1.1" + version: "7.2.0" connectivity_plus_platform_interface: dependency: transitive description: @@ -276,10 +276,10 @@ packages: dependency: "direct main" description: name: cross_file - sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" url: "https://pub.dev" source: hosted - version: "0.3.5+2" + version: "0.3.5+4" crypto: dependency: transitive description: @@ -349,10 +349,10 @@ packages: dependency: transitive description: name: device_info_plus - sha256: "6a642e1daa10190af89ba6cb6386c0df7d071a3592080bfe1e44faa63ae1df65" + sha256: "0891702f96b2e465fe567b7ec448380e6b1c14f60af552a8536d9f583b6b8442" url: "https://pub.dev" source: hosted - version: "13.1.0" + version: "13.2.0" device_info_plus_platform_interface: dependency: transitive description: @@ -373,10 +373,10 @@ packages: dependency: "direct main" description: name: equatable - sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + sha256: "3bce007a596ff8b3119c45d68aaef631272537c03d30e5d4534dd24bf4c5eaa2" url: "https://pub.dev" source: hosted - version: "2.0.8" + version: "2.1.0" fake_async: dependency: transitive description: @@ -413,10 +413,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: fc83774ce5bd7ce08168333b5e53dbe9090ec04eb21e7aa7cd7bac921032c934 + sha256: fdc6a37f715d19f35b131decf1ce39242eeed5ddae18c0818c3eccb731ab76be url: "https://pub.dev" source: hosted - version: "12.0.0-beta.5" + version: "12.0.0-beta.7" fixnum: dependency: transitive description: @@ -627,10 +627,10 @@ packages: dependency: transitive description: name: hooks - sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" + sha256: "03a564c3704524ee0f7fc56fc621e8796cc2eb8c24d1f1a33b34979815b285c4" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "2.1.0" html: dependency: "direct main" description: @@ -667,10 +667,10 @@ packages: dependency: "direct main" description: name: idb_shim - sha256: aabfe78d065fb7b40fffb337cc7c50757dd4eee3dec95cac2bd2692e7b874ef3 + sha256: "3448298f244bc76a14aca71461eeab6add2b8a877930521c42c2e8b71b644590" url: "https://pub.dev" source: hosted - version: "2.9.3" + version: "2.9.6+2" image: dependency: "direct main" description: @@ -802,8 +802,8 @@ packages: dependency: "direct main" description: path: "packages/lw_file_system" - ref: "687c9a21fbb45fe33aff6872838183af71e3c9c7" - resolved-ref: "687c9a21fbb45fe33aff6872838183af71e3c9c7" + ref: "18711cf5297f9679701536255dfe3d650b72fd25" + resolved-ref: "18711cf5297f9679701536255dfe3d650b72fd25" url: "https://github.com/LinwoodDev/dart_pkgs.git" source: git version: "1.0.0" @@ -811,8 +811,8 @@ packages: dependency: transitive description: path: "packages/lw_file_system_api" - ref: "5fd8a282404929e54c36a25fd80e43ca3a85bc46" - resolved-ref: "5fd8a282404929e54c36a25fd80e43ca3a85bc46" + ref: "37594211585889e35e65dad382d4fdcf29ac69a0" + resolved-ref: "37594211585889e35e65dad382d4fdcf29ac69a0" url: "https://github.com/LinwoodDev/dart_pkgs.git" source: git version: "1.0.0" @@ -853,8 +853,8 @@ packages: dependency: "direct main" description: path: "packages/material_leap" - ref: b12a98c7236116c39ed8f3f2bf7f7666163d4fdf - resolved-ref: b12a98c7236116c39ed8f3f2bf7f7666163d4fdf + ref: "064aed61c361194bf0eb3cb599e62bdd3c111d7c" + resolved-ref: "064aed61c361194bf0eb3cb599e62bdd3c111d7c" url: "https://github.com/LinwoodDev/dart_pkgs.git" source: git version: "0.0.1" @@ -886,18 +886,10 @@ packages: dependency: "direct dev" description: name: msix - sha256: b6b08e7a7b5d1845f2b1d31216d5b1fb558e98251efefe54eb79ed00d27bc2ac + sha256: "61415c352e8aea084332b8a5514e6408c961c8cdeca202804fff1040eb555ac7" url: "https://pub.dev" source: hosted - version: "3.16.13" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" - url: "https://pub.dev" - source: hosted - version: "0.17.6" + version: "3.18.0" nested: dependency: transitive description: @@ -910,10 +902,10 @@ packages: dependency: "direct main" description: name: network_info_plus - sha256: f424bad71994a1dc8594b00a6f71a665f7714ee7b32d397656d592c030d8555f + sha256: "4a1217d16644ed59f88e415e2777a4c81f4da5bffb724566825e5551c6379567" url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.2.0" network_info_plus_platform_interface: dependency: transitive description: @@ -961,10 +953,10 @@ packages: dependency: transitive description: name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.4.1" one_dollar_unistroke_recognizer: dependency: "direct main" description: @@ -985,10 +977,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "4bf625947f6c7713ee242296a682e23e44823c09cf9d79e4f1238923c92db852" + sha256: f5c435dc0e0d461e5b32471a870f769b6a1cc46930637efe24fbc535314e78ad url: "https://pub.dev" source: hosted - version: "10.1.0" + version: "10.2.0" package_info_plus_platform_interface: dependency: transitive description: @@ -1017,10 +1009,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.6" path_provider_android: dependency: transitive description: @@ -1041,18 +1033,18 @@ packages: dependency: transitive description: name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_windows: dependency: transitive description: @@ -1065,34 +1057,34 @@ packages: dependency: transitive description: name: pdfium_dart - sha256: b536c19a10f8c86c160274a54ca6f29bc4c88001324ddcda4cca316033f2dc96 + sha256: "86e95c66b09f3245b95c4924f2edce8d4f7c9786876e5c5ee8e36b104a94bbb0" url: "https://pub.dev" source: hosted - version: "0.2.4" + version: "0.2.5" pdfium_flutter: dependency: transitive description: name: pdfium_flutter - sha256: "97ccb5cf207b66ba5549f09047e935376d967c6311ec3c5314405d1d04569fd2" + sha256: "0b115c0917aef9cf6bb9c4d47d0efc61d97f44844cd76287c921f997c26cf15d" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.3" pdfrx: dependency: "direct main" description: name: pdfrx - sha256: "6b3571565fb412fb7d0a325a76154d0685bd0a0659c3f41ee007f408d48cfd46" + sha256: acce61944c31bd36f0c3031b1bbf41f59d0b0d4e5e27614fbdc74ff093d55464 url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.7" pdfrx_engine: dependency: transitive description: name: pdfrx_engine - sha256: a201b11e13b6c729d731ddc96ce06394cce8503c39e4c7e9d2fa51b3e7b351a5 + sha256: ef8f8cfa64255bfc91a559dd2d32d7cfe7a0f8003bac297bbf9a0aed1182fae5 url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "0.4.6" perfect_freehand: dependency: "direct main" description: @@ -1210,10 +1202,10 @@ packages: dependency: transitive description: name: record_use - sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + sha256: af37186ff9ede46fa32f152526c48f46c5b3ad7d3d5a566140cb5df3dc4ed737 url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "1.0.0" reorderable_grid: dependency: "direct main" description: @@ -1242,58 +1234,58 @@ packages: dependency: transitive description: name: screen_retriever - sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" + sha256: ace919117a7520c13a50a6259e60c4a0d4cbe98809468792a91b5c5adada2aa6 url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.2" screen_retriever_linux: dependency: transitive description: name: screen_retriever_linux - sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 + sha256: "7b52006a5ceae1f3d5af7f77188c3290d6e7d8ded16d99809bea84967c65c257" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.2" screen_retriever_macos: dependency: transitive description: name: screen_retriever_macos - sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" + sha256: a1489b99cce597c45a54b9aae1cd94c8d4705353b7e0bb2457a6e4de44e0ad8a url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.2" screen_retriever_platform_interface: dependency: transitive description: name: screen_retriever_platform_interface - sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 + sha256: "94a5535277510a63184ca178ce12a1449bc0b38618879aa1c18bf57369c5064a" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.2" screen_retriever_windows: dependency: transitive description: name: screen_retriever_windows - sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" + sha256: dafc6922b0bfbf1d48cf3ccbf519b4fff47bdcb820da1728ea6db675fecc9324 url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.2" sembast: dependency: transitive description: name: sembast - sha256: "9189258e31373c3e86e3f99889439e577c704f1acb9cedac4abf6f7a522040ba" + sha256: a58b26925e23071cf0f4754d8449aabe829e9a9930a872fb18e6ae4c2c88e025 url: "https://pub.dev" source: hosted - version: "3.8.8+1" + version: "3.8.9+1" share_plus: dependency: "direct main" description: name: share_plus - sha256: a857d8b1479250aff6b57a51b2c02d31ca05848d441817c43f1640c885c286c0 + sha256: "9eee8283462d91a7a1c8bdb67d08874abd75a2f8fae3bc0ca033035e375fb3d8" url: "https://pub.dev" source: hosted - version: "13.1.0" + version: "13.2.0" share_plus_platform_interface: dependency: transitive description: @@ -1314,10 +1306,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: a2c49fc1fed7140cadd892d765bd47edbe4ac0b9c7e7e3c493dcb58126f99cf0 + sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5" url: "https://pub.dev" source: hosted - version: "2.4.25" + version: "2.4.26" shared_preferences_foundation: dependency: transitive description: @@ -1641,10 +1633,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "7ee12e6dffe0fc8e755179d6d91b3b34f5924223fc104d85572ef9180d73d172" + sha256: "142a9146f447d15b10bdc00e21d5f4d83e5b32bb5f8f8f5a04c75311344923a3" url: "https://pub.dev" source: hosted - version: "1.2.5" + version: "1.2.6" vector_math: dependency: transitive description: @@ -1721,10 +1713,10 @@ packages: dependency: "direct main" description: name: window_manager - sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd" + sha256: "05c231fd7b23d2380f14c5cc10b7b93d60d4fa4a2fb4e0f032de27e44b5560e9" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.5.2" xdg_directories: dependency: transitive description: @@ -1750,5 +1742,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.12.0 <4.0.0" - flutter: "3.44.1" + dart: ">=3.12.2 <4.0.0" + flutter: "3.44.6" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 9aab71c9ad6f..81f383d58365 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -16,8 +16,8 @@ publish_to: none version: 2.5.3+189 environment: - sdk: ">=3.9.0 <4.0.0" - flutter: 3.44.1 + sdk: ">=3.12.2 <4.0.0" + flutter: 3.44.6 dependencies: flutter: diff --git a/tools/pubspec.lock b/tools/pubspec.lock index 58b4b4e12edd..8f753bc2b280 100644 --- a/tools/pubspec.lock +++ b/tools/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.20.3" lints: dependency: "direct main" description: @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: meta - sha256: df0c643f44ad098eb37988027a8e2b2b5a031fd3977f06bbfd3a76637e8df739 + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" url: "https://pub.dev" source: hosted - version: "1.18.2" + version: "1.19.0" path: dependency: transitive description: @@ -130,4 +130,4 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.0 <4.0.0" + dart: ">=3.9.0 <4.0.0" From bd31a952ae1ff578ee50470d4be42138cc6104fe Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 13 Jul 2026 20:11:35 +0200 Subject: [PATCH 080/117] Prepare the 2.5.4 hotfix release --- api/pubspec.yaml | 2 +- app/linux/debian/DEBIAN/control | 2 +- app/pubspec.lock | 8 ++++---- app/pubspec.yaml | 4 ++-- metadata/en-US/changelogs/189.txt | 17 +++++++++++++++++ 5 files changed, 25 insertions(+), 8 deletions(-) create mode 100644 metadata/en-US/changelogs/189.txt diff --git a/api/pubspec.yaml b/api/pubspec.yaml index ad7ae3cc1ebf..105772107978 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -1,6 +1,6 @@ name: butterfly_api description: The Linwood Butterfly API -version: 2.5.3 +version: 2.5.4 publish_to: none environment: diff --git a/app/linux/debian/DEBIAN/control b/app/linux/debian/DEBIAN/control index 00939425549f..ea772ee14e99 100644 --- a/app/linux/debian/DEBIAN/control +++ b/app/linux/debian/DEBIAN/control @@ -1,5 +1,5 @@ Package: linwood-butterfly -Version: 2.5.3 +Version: 2.5.4 Section: base Priority: optional Homepage: https://github.com/LinwoodDev/butterfly diff --git a/app/pubspec.lock b/app/pubspec.lock index ec09402407c9..f637bdb7d3c0 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -148,10 +148,10 @@ packages: dependency: "direct main" description: name: camera - sha256: "034c38cb8014d29698dcae6d20276688a1bf74e6487dfeb274d70ea05d5f7777" + sha256: "558230d6ce6ccea856b32d390db7e7b557adf4d9320aa614481bd3f2f608953f" url: "https://pub.dev" source: hosted - version: "0.12.0+1" + version: "0.12.0+2" camera_android_camerax: dependency: transitive description: @@ -1306,10 +1306,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5" + sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7" url: "https://pub.dev" source: hosted - version: "2.4.26" + version: "2.4.27" shared_preferences_foundation: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 81f383d58365..3c7243656123 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -13,7 +13,7 @@ publish_to: none # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 2.5.3+189 +version: 2.5.4+189 environment: sdk: ">=3.12.2 <4.0.0" @@ -92,7 +92,7 @@ dependencies: lw_file_system: git: url: https://github.com/LinwoodDev/dart_pkgs.git - ref: 687c9a21fbb45fe33aff6872838183af71e3c9c7 + ref: 18711cf5297f9679701536255dfe3d650b72fd25 path: packages/lw_file_system keybinder: git: diff --git a/metadata/en-US/changelogs/189.txt b/metadata/en-US/changelogs/189.txt new file mode 100644 index 000000000000..ea7852a6ee33 --- /dev/null +++ b/metadata/en-US/changelogs/189.txt @@ -0,0 +1,17 @@ +This is a hotfix update, cherry-picking important fixes from the latest 2.6.0 beta and nightly releases. + +Cherry picks: +* Prevent crashes when Android SAF handles large folders and files +* Fix text labels disappearing while editing +* Fix polygons disappearing while editing +* Fix renamed files appearing twice in Recent files +* Fix layers and pages being reordered to the wrong position +* Improve WebDAV compatibility and select the correct filesystem with SAF enabled +* Fix the spacer tool for circles, shapes, polygons, and other elements +* Fix repeated saves and mark imported documents as unsaved +* Fix locked zoom controls +* Respect hidden file extensions in Recent files +* Persist the thumbnail setting +* Correct the texture height label + +Read more here: https://linwood.dev/butterfly/2.5.4 From 61d8fe9d618e3cde973c0ef243b1750d0f326c11 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Fri, 10 Jul 2026 22:29:08 +0200 Subject: [PATCH 081/117] Add template custom file name --- api/lib/src/models/data.dart | 21 +++- api/lib/src/models/meta.dart | 21 ++++ api/lib/src/models/meta.freezed.dart | 27 ++--- api/lib/src/models/meta.g.dart | 2 + api/pubspec.lock | 16 +++ api/pubspec.yaml | 1 + api/test/data_test.dart | 45 ++++++++ app/lib/actions/new.dart | 29 ++++- app/lib/bloc/document_bloc.dart | 3 + app/lib/dialogs/template.dart | 152 ++++++++++++++++++++++----- app/lib/l10n/app_en.arb | 14 +++ app/test/actions/new_test.dart | 120 +++++++++++++++++++++ 12 files changed, 408 insertions(+), 43 deletions(-) create mode 100644 app/test/actions/new_test.dart diff --git a/api/lib/src/models/data.dart b/api/lib/src/models/data.dart index b40248e55a15..99d2aba89623 100644 --- a/api/lib/src/models/data.dart +++ b/api/lib/src/models/data.dart @@ -226,6 +226,7 @@ final class NoteData extends NoteDisplay { String? name, String? description, String? directory, + String? fileName, Uint8List? thumbnail, }) { // Copy archive @@ -241,6 +242,7 @@ final class NoteData extends NoteDisplay { description: description ?? metadata?.description ?? '', fileVersion: kFileVersion, directory: directory ?? metadata?.directory ?? '', + fileName: fileName ?? metadata?.fileName ?? '', ); template = template.setMetadata(newMetadata); if (thumbnail != null) template = template.setThumbnail(thumbnail); @@ -256,10 +258,25 @@ final class NoteData extends NoteDisplay { final archive = export(); var document = NoteData(archive); final metadata = getMetadata(); - createdAt ??= DateTime.now().toUtc(); + final now = DateTime.now(); + final fileNameDate = createdAt?.toLocal() ?? now; + createdAt ??= now.toUtc(); + var documentName = name; + if (documentName.isEmpty && metadata?.type == NoteFileType.template) { + try { + documentName = resolveTemplateFileName( + metadata?.fileName ?? '', + fileNameDate, + ); + } on FormatException { + // Invalid formatters from imported templates should keep the document + // unsaved instead of preventing it from opening. + documentName = ''; + } + } final newMetadata = FileMetadata( type: NoteFileType.document, - name: name, + name: documentName, createdAt: createdAt, updatedAt: createdAt, description: metadata?.description ?? '', diff --git a/api/lib/src/models/meta.dart b/api/lib/src/models/meta.dart index cf173aba6639..4b7283066f84 100644 --- a/api/lib/src/models/meta.dart +++ b/api/lib/src/models/meta.dart @@ -1,4 +1,5 @@ import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:intl/intl.dart'; import '../converter/core.dart'; @@ -7,6 +8,25 @@ part 'meta.g.dart'; const kFileVersion = 13; const kBreakingChangesVersion = 7; +const templateDateFormatExample = '{date:dd.MM.yyyy}'; +const templateTimeFormatExample = '{time:HH-mm}'; +final _templateDateTimePattern = RegExp(r'\{(?:date|time):([^{}]+)\}'); + +/// Resolves the supported placeholders in a template's document file name. +/// +/// Each placeholder contains an ICU date format, for example +/// `{date:dd.MM.yyyy}` or `{time:HH-mm}`. The caller remains responsible for +/// validating that the formatted result is a valid file name. +String resolveTemplateFileName(String pattern, DateTime dateTime) { + final resolved = pattern.replaceAllMapped(_templateDateTimePattern, (match) { + final format = match.group(1)!; + return DateFormat(format).format(dateTime); + }); + if (resolved.contains('{date:') || resolved.contains('{time:')) { + throw FormatException('Invalid template date or time formatter', pattern); + } + return resolved.trim(); +} @freezed sealed class FileMetadata with _$FileMetadata { @@ -19,6 +39,7 @@ sealed class FileMetadata with _$FileMetadata { @Default('') String description, @Default('') String author, @Default('') String directory, + @Default('') String fileName, @Default('') String version, }) = _FileMetadata; diff --git a/api/lib/src/models/meta.freezed.dart b/api/lib/src/models/meta.freezed.dart index 944398f9c024..6ab8ebe54956 100644 --- a/api/lib/src/models/meta.freezed.dart +++ b/api/lib/src/models/meta.freezed.dart @@ -15,7 +15,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$FileMetadata { - int? get fileVersion; NoteFileType get type;@DateTimeJsonConverter() DateTime? get createdAt;@DateTimeJsonConverter() DateTime? get updatedAt; String get name; String get description; String get author; String get directory; String get version; + int? get fileVersion; NoteFileType get type;@DateTimeJsonConverter() DateTime? get createdAt;@DateTimeJsonConverter() DateTime? get updatedAt; String get name; String get description; String get author; String get directory; String get fileName; String get version; /// Create a copy of FileMetadata /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -28,16 +28,16 @@ $FileMetadataCopyWith get copyWith => _$FileMetadataCopyWithImpl Object.hash(runtimeType,fileVersion,type,createdAt,updatedAt,name,description,author,directory,version); +int get hashCode => Object.hash(runtimeType,fileVersion,type,createdAt,updatedAt,name,description,author,directory,fileName,version); @override String toString() { - return 'FileMetadata(fileVersion: $fileVersion, type: $type, createdAt: $createdAt, updatedAt: $updatedAt, name: $name, description: $description, author: $author, directory: $directory, version: $version)'; + return 'FileMetadata(fileVersion: $fileVersion, type: $type, createdAt: $createdAt, updatedAt: $updatedAt, name: $name, description: $description, author: $author, directory: $directory, fileName: $fileName, version: $version)'; } @@ -48,7 +48,7 @@ abstract mixin class $FileMetadataCopyWith<$Res> { factory $FileMetadataCopyWith(FileMetadata value, $Res Function(FileMetadata) _then) = _$FileMetadataCopyWithImpl; @useResult $Res call({ - int? fileVersion, NoteFileType type,@DateTimeJsonConverter() DateTime? createdAt,@DateTimeJsonConverter() DateTime? updatedAt, String name, String description, String author, String directory, String version + int? fileVersion, NoteFileType type,@DateTimeJsonConverter() DateTime? createdAt,@DateTimeJsonConverter() DateTime? updatedAt, String name, String description, String author, String directory, String fileName, String version }); @@ -65,7 +65,7 @@ class _$FileMetadataCopyWithImpl<$Res> /// Create a copy of FileMetadata /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? fileVersion = freezed,Object? type = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? name = null,Object? description = null,Object? author = null,Object? directory = null,Object? version = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? fileVersion = freezed,Object? type = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? name = null,Object? description = null,Object? author = null,Object? directory = null,Object? fileName = null,Object? version = null,}) { return _then(_self.copyWith( fileVersion: freezed == fileVersion ? _self.fileVersion : fileVersion // ignore: cast_nullable_to_non_nullable as int?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable @@ -75,6 +75,7 @@ as DateTime?,name: null == name ? _self.name : name // ignore: cast_nullable_to_ as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable as String,author: null == author ? _self.author : author // ignore: cast_nullable_to_non_nullable as String,directory: null == directory ? _self.directory : directory // ignore: cast_nullable_to_non_nullable +as String,fileName: null == fileName ? _self.fileName : fileName // ignore: cast_nullable_to_non_nullable as String,version: null == version ? _self.version : version // ignore: cast_nullable_to_non_nullable as String, )); @@ -88,7 +89,7 @@ as String, @JsonSerializable() class _FileMetadata implements FileMetadata { - const _FileMetadata({this.fileVersion, required this.type, @DateTimeJsonConverter() this.createdAt, @DateTimeJsonConverter() this.updatedAt, this.name = '', this.description = '', this.author = '', this.directory = '', this.version = ''}); + const _FileMetadata({this.fileVersion, required this.type, @DateTimeJsonConverter() this.createdAt, @DateTimeJsonConverter() this.updatedAt, this.name = '', this.description = '', this.author = '', this.directory = '', this.fileName = '', this.version = ''}); factory _FileMetadata.fromJson(Map json) => _$FileMetadataFromJson(json); @override final int? fileVersion; @@ -99,6 +100,7 @@ class _FileMetadata implements FileMetadata { @override@JsonKey() final String description; @override@JsonKey() final String author; @override@JsonKey() final String directory; +@override@JsonKey() final String fileName; @override@JsonKey() final String version; /// Create a copy of FileMetadata @@ -114,16 +116,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _FileMetadata&&(identical(other.fileVersion, fileVersion) || other.fileVersion == fileVersion)&&(identical(other.type, type) || other.type == type)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.author, author) || other.author == author)&&(identical(other.directory, directory) || other.directory == directory)&&(identical(other.version, version) || other.version == version)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _FileMetadata&&(identical(other.fileVersion, fileVersion) || other.fileVersion == fileVersion)&&(identical(other.type, type) || other.type == type)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.author, author) || other.author == author)&&(identical(other.directory, directory) || other.directory == directory)&&(identical(other.fileName, fileName) || other.fileName == fileName)&&(identical(other.version, version) || other.version == version)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,fileVersion,type,createdAt,updatedAt,name,description,author,directory,version); +int get hashCode => Object.hash(runtimeType,fileVersion,type,createdAt,updatedAt,name,description,author,directory,fileName,version); @override String toString() { - return 'FileMetadata(fileVersion: $fileVersion, type: $type, createdAt: $createdAt, updatedAt: $updatedAt, name: $name, description: $description, author: $author, directory: $directory, version: $version)'; + return 'FileMetadata(fileVersion: $fileVersion, type: $type, createdAt: $createdAt, updatedAt: $updatedAt, name: $name, description: $description, author: $author, directory: $directory, fileName: $fileName, version: $version)'; } @@ -134,7 +136,7 @@ abstract mixin class _$FileMetadataCopyWith<$Res> implements $FileMetadataCopyWi factory _$FileMetadataCopyWith(_FileMetadata value, $Res Function(_FileMetadata) _then) = __$FileMetadataCopyWithImpl; @override @useResult $Res call({ - int? fileVersion, NoteFileType type,@DateTimeJsonConverter() DateTime? createdAt,@DateTimeJsonConverter() DateTime? updatedAt, String name, String description, String author, String directory, String version + int? fileVersion, NoteFileType type,@DateTimeJsonConverter() DateTime? createdAt,@DateTimeJsonConverter() DateTime? updatedAt, String name, String description, String author, String directory, String fileName, String version }); @@ -151,7 +153,7 @@ class __$FileMetadataCopyWithImpl<$Res> /// Create a copy of FileMetadata /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? fileVersion = freezed,Object? type = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? name = null,Object? description = null,Object? author = null,Object? directory = null,Object? version = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? fileVersion = freezed,Object? type = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? name = null,Object? description = null,Object? author = null,Object? directory = null,Object? fileName = null,Object? version = null,}) { return _then(_FileMetadata( fileVersion: freezed == fileVersion ? _self.fileVersion : fileVersion // ignore: cast_nullable_to_non_nullable as int?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable @@ -161,6 +163,7 @@ as DateTime?,name: null == name ? _self.name : name // ignore: cast_nullable_to_ as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable as String,author: null == author ? _self.author : author // ignore: cast_nullable_to_non_nullable as String,directory: null == directory ? _self.directory : directory // ignore: cast_nullable_to_non_nullable +as String,fileName: null == fileName ? _self.fileName : fileName // ignore: cast_nullable_to_non_nullable as String,version: null == version ? _self.version : version // ignore: cast_nullable_to_non_nullable as String, )); diff --git a/api/lib/src/models/meta.g.dart b/api/lib/src/models/meta.g.dart index 43a2012ffe8c..2413dd6b61ab 100644 --- a/api/lib/src/models/meta.g.dart +++ b/api/lib/src/models/meta.g.dart @@ -21,6 +21,7 @@ _FileMetadata _$FileMetadataFromJson(Map json) => _FileMetadata( description: json['description'] as String? ?? '', author: json['author'] as String? ?? '', directory: json['directory'] as String? ?? '', + fileName: json['fileName'] as String? ?? '', version: json['version'] as String? ?? '', ); @@ -40,6 +41,7 @@ Map _$FileMetadataToJson(_FileMetadata instance) => 'description': instance.description, 'author': instance.author, 'directory': instance.directory, + 'fileName': instance.fileName, 'version': instance.version, }; diff --git a/api/pubspec.lock b/api/pubspec.lock index ed4d48100608..960d2281bae7 100644 --- a/api/pubspec.lock +++ b/api/pubspec.lock @@ -121,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" collection: dependency: "direct main" description: @@ -258,6 +266,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" + url: "https://pub.dev" + source: hosted + version: "0.20.3" io: dependency: transitive description: diff --git a/api/pubspec.yaml b/api/pubspec.yaml index e689d70c9dc3..6d8378a6c1ad 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -10,6 +10,7 @@ dependencies: archive: ^4.0.0 collection: ^1.18.0 freezed_annotation: ^3.0.0 + intl: any json_annotation: ^4.12.0 replay_bloc: ^0.3.0 rxdart: ^0.28.0 diff --git a/api/test/data_test.dart b/api/test/data_test.dart index ba4b2774986f..17c85d836e8a 100644 --- a/api/test/data_test.dart +++ b/api/test/data_test.dart @@ -222,6 +222,7 @@ void main() { name: 'Template Name', description: 'Template Description', directory: 'templates', + fileName: 'Note {date:dd.MM.yyyy} {time:HH-mm}', thumbnail: thumbnail, ); @@ -231,9 +232,53 @@ void main() { expect(metadata.name, 'Template Name'); expect(metadata.description, 'Template Description'); expect(metadata.directory, 'templates'); + expect(metadata.fileName, 'Note {date:dd.MM.yyyy} {time:HH-mm}'); expect(template.getThumbnail(), equals(thumbnail)); }); + test('createDocument resolves template file name placeholders', () { + var data = NoteData(Archive()); + data = data.setMetadata( + FileMetadata( + type: NoteFileType.template, + fileName: 'Daily {date:dd.MM.yyyy} {time:HH-mm-ss}', + ), + ); + final createdAt = DateTime(2026, 7, 10, 9, 8, 7); + + final document = data.createDocument(createdAt: createdAt); + + expect(document.getMetadata()?.name, 'Daily 10.07.2026 09-08-07'); + }); + + test('explicit document name overrides template file name', () { + var data = NoteData(Archive()); + data = data.setMetadata( + const FileMetadata( + type: NoteFileType.template, + fileName: 'Daily {date:yyyy-MM-dd}', + ), + ); + + final document = data.createDocument(name: 'Custom name'); + + expect(document.getMetadata()?.name, 'Custom name'); + }); + + test('invalid template date format keeps the document unnamed', () { + var data = NoteData(Archive()); + data = data.setMetadata( + const FileMetadata( + type: NoteFileType.template, + fileName: 'Daily {date:}', + ), + ); + + final document = data.createDocument(); + + expect(document.getMetadata()?.name, isEmpty); + }); + test('createDocument with disablePages removes all pages', () { var data = NoteData(Archive()); (data, _) = data.setPage(_pageWithLayer('one'), 'Page 1'); diff --git a/app/lib/actions/new.dart b/app/lib/actions/new.dart index fdcc0a311b4d..f3d6fb45af6e 100644 --- a/app/lib/actions/new.dart +++ b/app/lib/actions/new.dart @@ -56,7 +56,7 @@ class NewAction extends Action { final template = await templateSystem.getDefaultFile( templateSystem.storage?.defaults['template'] ?? settings.defaultTemplate, ); - openNewDocument(context, true, template); + await openNewDocument(context, true, template); } } @@ -66,9 +66,10 @@ Future openNewDocument( NoteData? template, String? remote, Area? initialArea, -]) { +]) async { NoteData? document; String? path; + var targetRemote = remote; if (template != null) { document = template.createDocument(); if (initialArea != null) { @@ -80,16 +81,34 @@ Future openNewDocument( final metadata = document.getMetadata(); if (metadata != null) { path = metadata.directory; + final templateMetadata = template.getMetadata(); + if ((templateMetadata?.fileName.trim().isNotEmpty ?? false) && + metadata.name.isNotEmpty) { + final settings = context.read().state; + final storage = settings.getRemote(targetRemote); + final fileSystem = context + .read() + .buildDocumentSystem(storage); + final created = await fileSystem.createFileWithName( + directory: path, + name: metadata.name, + suffix: '.bfly', + document.toFile(), + ); + path = created.path; + targetRemote = created.remote; + } } } - final queryParams = {'path': ?path, 'remote': ?remote}; + if (!context.mounted) return; + final queryParams = {'path': ?path, 'remote': ?targetRemote}; if (replace) { GoRouter.of( context, ).goNamed('new', queryParameters: queryParams, extra: document); - return Future.value(); + return; } else { - return GoRouter.of( + await GoRouter.of( context, ).pushNamed('new', queryParameters: queryParams, extra: document); } diff --git a/app/lib/bloc/document_bloc.dart b/app/lib/bloc/document_bloc.dart index 2cf5e48fd69d..6b7556ff2d88 100644 --- a/app/lib/bloc/document_bloc.dart +++ b/app/lib/bloc/document_bloc.dart @@ -1822,6 +1822,7 @@ class DocumentBloc extends ReplayBloc { String? remote, { String? directory, String? name, + String? fileName, }) async { final current = state; final cubit = _editorController; @@ -1849,6 +1850,7 @@ class DocumentBloc extends ReplayBloc { name: name, thumbnail: thumbnail, directory: directory, + fileName: fileName, ), ); } @@ -1868,6 +1870,7 @@ class DocumentBloc extends ReplayBloc { name: metadata.name, description: metadata.description, directory: metadata.directory, + fileName: metadata.fileName, ), ); } diff --git a/app/lib/dialogs/template.dart b/app/lib/dialogs/template.dart index aa15967b4e29..b3563e3c8a3a 100644 --- a/app/lib/dialogs/template.dart +++ b/app/lib/dialogs/template.dart @@ -24,6 +24,69 @@ import 'area/init.dart'; import 'delete.dart'; import 'pages.dart'; +String? _validateOptionalTemplateFileName(BuildContext context, String? value) { + final fileName = value?.trim() ?? ''; + if (fileName.isEmpty) return null; + try { + final resolved = resolveTemplateFileName( + fileName, + DateTime(2000, 12, 31, 23, 59, 58), + ); + return defaultFileNameValidator(context)(null)(resolved); + } on FormatException { + return LeapLocalizations.of(context).invalidName; + } +} + +String _templateFileNameDescription(BuildContext context) => + AppLocalizations.of(context).templateFileNameDescription( + templateDateFormatExample, + templateTimeFormatExample, + ); + +Future _showTemplateFileNameDialog( + BuildContext context, + String initialValue, +) { + final formKey = GlobalKey(); + var fileName = initialValue; + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(AppLocalizations.of(context).fileName), + content: Form( + key: formKey, + child: TextFormField( + initialValue: fileName, + autofocus: true, + onChanged: (value) => fileName = value, + validator: (value) => + _validateOptionalTemplateFileName(context, value), + decoration: InputDecoration( + labelText: AppLocalizations.of(context).fileName, + helperText: _templateFileNameDescription(context), + helperMaxLines: 3, + filled: true, + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), + ), + ElevatedButton( + onPressed: () { + if (!(formKey.currentState?.validate() ?? false)) return; + Navigator.of(context).pop(fileName.trim()); + }, + child: Text(MaterialLocalizations.of(context).saveButtonLabel), + ), + ], + ), + ); +} + Future _overrideTools( TemplateFileSystem templateSystem, DocumentBloc bloc, @@ -669,42 +732,59 @@ class _TemplateDialogState extends State { Future _showCreateDialog(DocumentBloc bloc) { final state = bloc.state; + final formKey = GlobalKey(); var initialName = ''; if (state is DocumentLoaded) { initialName = state.metadata.name; } - String name = initialName, directory = ''; + String name = initialName, directory = '', fileName = ''; return showDialog( context: context, builder: (context) { return AlertDialog( title: Text(AppLocalizations.of(context).createTemplate), scrollable: true, - content: SizedBox( - width: 500, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(AppLocalizations.of(context).createTemplateContent), - const SizedBox(height: 16), - TextFormField( - initialValue: name, - onChanged: (e) => name = e, - decoration: InputDecoration( - labelText: LeapLocalizations.of(context).name, - filled: true, + content: Form( + key: formKey, + child: SizedBox( + width: 500, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(AppLocalizations.of(context).createTemplateContent), + const SizedBox(height: 16), + TextFormField( + initialValue: name, + onChanged: (e) => name = e, + decoration: InputDecoration( + labelText: LeapLocalizations.of(context).name, + filled: true, + ), ), - ), - const SizedBox(height: 8), - TextFormField( - initialValue: directory, - onChanged: (e) => directory = e, - decoration: InputDecoration( - labelText: AppLocalizations.of(context).directory, - filled: true, + const SizedBox(height: 8), + TextFormField( + initialValue: fileName, + onChanged: (e) => fileName = e, + validator: (value) => + _validateOptionalTemplateFileName(context, value), + decoration: InputDecoration( + labelText: AppLocalizations.of(context).fileName, + helperText: _templateFileNameDescription(context), + helperMaxLines: 3, + filled: true, + ), ), - ), - ], + const SizedBox(height: 8), + TextFormField( + initialValue: directory, + onChanged: (e) => directory = e, + decoration: InputDecoration( + labelText: AppLocalizations.of(context).directory, + filled: true, + ), + ), + ], + ), ), ), actions: [ @@ -715,11 +795,13 @@ class _TemplateDialogState extends State { ElevatedButton( child: Text(LeapLocalizations.of(context).create), onPressed: () async { + if (!(formKey.currentState?.validate() ?? false)) return; Navigator.of(context).pop(); await bloc.createTemplate( _templateSystem.storage?.identifier, name: name, directory: directory, + fileName: fileName.trim(), ); load(); }, @@ -818,6 +900,11 @@ class _TemplateDetailsViewState extends State<_TemplateDetailsView> { title: Text(AppLocalizations.of(context).directory), subtitle: Text(metadata.directory), ), + if (metadata.fileName.isNotEmpty) + ListTile( + title: Text(AppLocalizations.of(context).fileName), + subtitle: Text(metadata.fileName), + ), ListTile( title: Text(AppLocalizations.of(context).tools), subtitle: Card.outlined( @@ -1380,6 +1467,23 @@ List _buildTemplateMenuChildren( _applyTemplateAreasToPages(bloc, template, selectedPageNames); }, ), + if (!isCore) + MenuItemButton( + leadingIcon: const PhosphorIcon(PhosphorIconsLight.fileText), + child: Text(AppLocalizations.of(context).fileName), + onPressed: () async { + final result = await _showTemplateFileNameDialog( + context, + metadata.fileName, + ); + if (result == null) return; + await fileSystem.updateFile( + file.path, + template.setMetadata(metadata.copyWith(fileName: result)), + ); + onChanged(); + }, + ), MenuItemButton( leadingIcon: const PhosphorIcon(PhosphorIconsLight.copy), child: Text(AppLocalizations.of(context).duplicate), diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 764f06bd6b2e..37083b3e23bb 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -350,6 +350,20 @@ "untitled": "Untitled", "createTemplate": "Create template", "createTemplateContent": "Do you really want to create a template from this document? The original document will get deleted.", + "fileName": "File name", + "templateFileNameDescription": "Set a name to save new documents immediately. Use a custom date or time formatter, for example {dateExample} or {timeExample}.", + "@templateFileNameDescription": { + "placeholders": { + "dateExample": { + "type": "String", + "example": "{date:dd.MM.yyyy}" + }, + "timeExample": { + "type": "String", + "example": "{time:HH-mm}" + } + } + }, "replace": "Replace", "@replace": { "description": "Replace action" diff --git a/app/test/actions/new_test.dart b/app/test/actions/new_test.dart new file mode 100644 index 000000000000..f8b5ef1c7f8a --- /dev/null +++ b/app/test/actions/new_test.dart @@ -0,0 +1,120 @@ +import 'package:archive/archive.dart'; +import 'package:butterfly/actions/new.dart'; +import 'package:butterfly/api/file_system.dart'; +import 'package:butterfly/cubits/settings.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; +import 'package:lw_file_system/lw_file_system.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../helpers/mocks.dart'; + +void main() { + late MockSettingsCubit settingsCubit; + late MockButterflyFileSystem fileSystem; + late GoRouter router; + String? openedPath; + String? openedRemote; + Object? openedData; + + setUp(() { + settingsCubit = MockSettingsCubit(); + fileSystem = MockButterflyFileSystem(settingsCubit: settingsCubit); + openedPath = null; + openedRemote = null; + openedData = null; + + when(() => settingsCubit.state).thenReturn(const ButterflySettings()); + when(() => settingsCubit.stream).thenAnswer((_) => const Stream.empty()); + }); + + tearDown(() => router.dispose()); + + Widget buildApp(NoteData template) { + router = GoRouter( + initialLocation: '/', + routes: [ + GoRoute( + path: '/', + builder: (context, state) => Scaffold( + body: TextButton( + onPressed: () => openNewDocument(context, true, template), + child: const Text('Create'), + ), + ), + ), + GoRoute( + name: 'new', + path: '/new', + builder: (context, state) { + openedPath = state.uri.queryParameters['path']; + openedRemote = state.uri.queryParameters['remote']; + openedData = state.extra; + return const Scaffold(body: Text('Opened')); + }, + ), + ], + ); + + return RepositoryProvider.value( + value: fileSystem, + child: BlocProvider.value( + value: settingsCubit, + child: MaterialApp.router(routerConfig: router), + ), + ); + } + + testWidgets('template file name creates the document before opening it', ( + tester, + ) async { + var template = NoteData(Archive()); + template = template.setMetadata( + const FileMetadata( + type: NoteFileType.template, + name: 'Journal template', + directory: 'journals', + fileName: 'Journal entry', + ), + ); + + await tester.pumpWidget(buildApp(template)); + await tester.tap(find.text('Create')); + await tester.pumpAndSettle(); + + expect(find.text('Opened'), findsOneWidget); + expect(openedPath, endsWith('journals/Journal entry.bfly')); + expect(openedRemote, ''); + expect((openedData as NoteData).getMetadata()?.name, 'Journal entry'); + + final asset = await fileSystem.buildDocumentSystem().getAsset(openedPath!); + expect(asset, isA>()); + final saved = (asset as FileSystemFile).data?.display(); + expect(saved?.getMetadata()?.name, 'Journal entry'); + }); + + testWidgets('template without a file name keeps the document unsaved', ( + tester, + ) async { + var template = NoteData(Archive()); + template = template.setMetadata( + const FileMetadata( + type: NoteFileType.template, + name: 'Legacy template', + directory: 'journals', + ), + ); + + await tester.pumpWidget(buildApp(template)); + await tester.tap(find.text('Create')); + await tester.pumpAndSettle(); + + expect(find.text('Opened'), findsOneWidget); + expect(openedPath, 'journals'); + expect((openedData as NoteData).getMetadata()?.name, isEmpty); + expect(await fileSystem.buildDocumentSystem().getAsset('journals'), isNull); + }); +} From 6f86805d59b55326b9572e8c3b1d4f06528dae2a Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 14 Jul 2026 10:59:51 +0200 Subject: [PATCH 082/117] Add default file name setting --- api/lib/src/models/data.dart | 19 +- api/lib/src/models/meta.dart | 20 -- api/pubspec.lock | 16 -- api/pubspec.yaml | 1 - api/test/data_test.dart | 43 ---- app/lib/actions/new.dart | 21 +- app/lib/api/save.dart | 27 +++ app/lib/cubits/settings.dart | 15 ++ app/lib/cubits/settings.freezed.dart | 31 +-- app/lib/cubits/settings.g.dart | 2 + app/lib/dialogs/template.dart | 80 +------- app/lib/l10n/app_en.arb | 8 +- app/lib/settings/data.dart | 36 ++++ app/lib/settings/pages/data.dart | 5 + app/lib/widgets/file_name_pattern_field.dart | 188 ++++++++++++++++++ app/test/actions/new_test.dart | 33 ++- app/test/api/save_test.dart | 27 +++ app/test/cubits/settings_test.dart | 35 ++++ .../widgets/file_name_pattern_field_test.dart | 51 +++++ 19 files changed, 461 insertions(+), 197 deletions(-) create mode 100644 app/lib/widgets/file_name_pattern_field.dart create mode 100644 app/test/widgets/file_name_pattern_field_test.dart diff --git a/api/lib/src/models/data.dart b/api/lib/src/models/data.dart index 99d2aba89623..d50ff7bbd4fa 100644 --- a/api/lib/src/models/data.dart +++ b/api/lib/src/models/data.dart @@ -258,25 +258,10 @@ final class NoteData extends NoteDisplay { final archive = export(); var document = NoteData(archive); final metadata = getMetadata(); - final now = DateTime.now(); - final fileNameDate = createdAt?.toLocal() ?? now; - createdAt ??= now.toUtc(); - var documentName = name; - if (documentName.isEmpty && metadata?.type == NoteFileType.template) { - try { - documentName = resolveTemplateFileName( - metadata?.fileName ?? '', - fileNameDate, - ); - } on FormatException { - // Invalid formatters from imported templates should keep the document - // unsaved instead of preventing it from opening. - documentName = ''; - } - } + createdAt ??= DateTime.now().toUtc(); final newMetadata = FileMetadata( type: NoteFileType.document, - name: documentName, + name: name, createdAt: createdAt, updatedAt: createdAt, description: metadata?.description ?? '', diff --git a/api/lib/src/models/meta.dart b/api/lib/src/models/meta.dart index 4b7283066f84..3d2d8220d29f 100644 --- a/api/lib/src/models/meta.dart +++ b/api/lib/src/models/meta.dart @@ -1,5 +1,4 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:intl/intl.dart'; import '../converter/core.dart'; @@ -8,25 +7,6 @@ part 'meta.g.dart'; const kFileVersion = 13; const kBreakingChangesVersion = 7; -const templateDateFormatExample = '{date:dd.MM.yyyy}'; -const templateTimeFormatExample = '{time:HH-mm}'; -final _templateDateTimePattern = RegExp(r'\{(?:date|time):([^{}]+)\}'); - -/// Resolves the supported placeholders in a template's document file name. -/// -/// Each placeholder contains an ICU date format, for example -/// `{date:dd.MM.yyyy}` or `{time:HH-mm}`. The caller remains responsible for -/// validating that the formatted result is a valid file name. -String resolveTemplateFileName(String pattern, DateTime dateTime) { - final resolved = pattern.replaceAllMapped(_templateDateTimePattern, (match) { - final format = match.group(1)!; - return DateFormat(format).format(dateTime); - }); - if (resolved.contains('{date:') || resolved.contains('{time:')) { - throw FormatException('Invalid template date or time formatter', pattern); - } - return resolved.trim(); -} @freezed sealed class FileMetadata with _$FileMetadata { diff --git a/api/pubspec.lock b/api/pubspec.lock index 960d2281bae7..ed4d48100608 100644 --- a/api/pubspec.lock +++ b/api/pubspec.lock @@ -121,14 +121,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" collection: dependency: "direct main" description: @@ -266,14 +258,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" - intl: - dependency: "direct main" - description: - name: intl - sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" - url: "https://pub.dev" - source: hosted - version: "0.20.3" io: dependency: transitive description: diff --git a/api/pubspec.yaml b/api/pubspec.yaml index 6d8378a6c1ad..e689d70c9dc3 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -10,7 +10,6 @@ dependencies: archive: ^4.0.0 collection: ^1.18.0 freezed_annotation: ^3.0.0 - intl: any json_annotation: ^4.12.0 replay_bloc: ^0.3.0 rxdart: ^0.28.0 diff --git a/api/test/data_test.dart b/api/test/data_test.dart index 17c85d836e8a..6ece04f6b9de 100644 --- a/api/test/data_test.dart +++ b/api/test/data_test.dart @@ -236,49 +236,6 @@ void main() { expect(template.getThumbnail(), equals(thumbnail)); }); - test('createDocument resolves template file name placeholders', () { - var data = NoteData(Archive()); - data = data.setMetadata( - FileMetadata( - type: NoteFileType.template, - fileName: 'Daily {date:dd.MM.yyyy} {time:HH-mm-ss}', - ), - ); - final createdAt = DateTime(2026, 7, 10, 9, 8, 7); - - final document = data.createDocument(createdAt: createdAt); - - expect(document.getMetadata()?.name, 'Daily 10.07.2026 09-08-07'); - }); - - test('explicit document name overrides template file name', () { - var data = NoteData(Archive()); - data = data.setMetadata( - const FileMetadata( - type: NoteFileType.template, - fileName: 'Daily {date:yyyy-MM-dd}', - ), - ); - - final document = data.createDocument(name: 'Custom name'); - - expect(document.getMetadata()?.name, 'Custom name'); - }); - - test('invalid template date format keeps the document unnamed', () { - var data = NoteData(Archive()); - data = data.setMetadata( - const FileMetadata( - type: NoteFileType.template, - fileName: 'Daily {date:}', - ), - ); - - final document = data.createDocument(); - - expect(document.getMetadata()?.name, isEmpty); - }); - test('createDocument with disablePages removes all pages', () { var data = NoteData(Archive()); (data, _) = data.setPage(_pageWithLayer('one'), 'Page 1'); diff --git a/app/lib/actions/new.dart b/app/lib/actions/new.dart index f3d6fb45af6e..df9d141cdc90 100644 --- a/app/lib/actions/new.dart +++ b/app/lib/actions/new.dart @@ -1,4 +1,5 @@ import 'package:butterfly/api/file_system.dart'; +import 'package:butterfly/api/save.dart'; import 'package:butterfly/bloc/document_bloc.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/models/defaults.dart'; @@ -71,7 +72,20 @@ Future openNewDocument( String? path; var targetRemote = remote; if (template != null) { - document = template.createDocument(); + final settings = context.read().state; + final templatePattern = template.getMetadata()?.fileName.trim() ?? ''; + final fileNamePattern = templatePattern.isNotEmpty + ? templatePattern + : settings.defaultFileName.trim(); + var documentName = ''; + if (fileNamePattern.isNotEmpty) { + try { + documentName = resolveTemplateFileName(fileNamePattern, DateTime.now()); + } on FormatException { + // Invalid patterns from imported templates should still open unsaved. + } + } + document = template.createDocument(name: documentName); if (initialArea != null) { final page = document.getPage() ?? DocumentDefaults.createPage(); document = document @@ -81,10 +95,7 @@ Future openNewDocument( final metadata = document.getMetadata(); if (metadata != null) { path = metadata.directory; - final templateMetadata = template.getMetadata(); - if ((templateMetadata?.fileName.trim().isNotEmpty ?? false) && - metadata.name.isNotEmpty) { - final settings = context.read().state; + if (fileNamePattern.isNotEmpty && metadata.name.isNotEmpty) { final storage = settings.getRemote(targetRemote); final fileSystem = context .read() diff --git a/app/lib/api/save.dart b/app/lib/api/save.dart index 2ca1eb1d2464..c7aa6ca811ed 100644 --- a/app/lib/api/save.dart +++ b/app/lib/api/save.dart @@ -7,6 +7,33 @@ import 'package:flutter/material.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; import 'package:lw_sysapi/lw_sysapi.dart'; import 'package:lw_file_system/lw_file_system.dart'; +import 'package:intl/intl.dart'; + +const templateDateFormatExample = '{date}'; +const templateTimeFormatExample = '{time}'; +final _templateDateTimePattern = RegExp(r'\{(?:date|time):([^{}]+)\}'); + +String resolveTemplateFileName(String pattern, DateTime dateTime) { + final simple = pattern + .replaceAll('{date}', DateFormat('yyyy-MM-dd').format(dateTime)) + .replaceAll('{time}', DateFormat('HH-mm').format(dateTime)); + final resolved = simple.replaceAllMapped(_templateDateTimePattern, (match) { + return DateFormat(match.group(1)!).format(dateTime); + }); + if (resolved.contains('{date:') || resolved.contains('{time:')) { + throw FormatException('Invalid template date or time formatter', pattern); + } + return resolved.trim(); +} + +String? previewTemplateFileName(String pattern, [DateTime? dateTime]) { + if (pattern.trim().isEmpty) return null; + try { + return resolveTemplateFileName(pattern, dateTime ?? DateTime.now()); + } on FormatException { + return null; + } +} String sanitizeExportFileName(String? name) => convertNameToFile( name: name?.trim(), diff --git a/app/lib/cubits/settings.dart b/app/lib/cubits/settings.dart index f23d336e560c..cb341e0af834 100644 --- a/app/lib/cubits/settings.dart +++ b/app/lib/cubits/settings.dart @@ -20,6 +20,7 @@ part 'settings.g.dart'; const secureStorage = FlutterSecureStorage(); const kRecentHistorySize = 5; +const kDefaultFileName = '{date}'; String _normalizeCachePath(String path) { if (path.endsWith('/')) { @@ -497,6 +498,7 @@ sealed class ButterflySettings with _$ButterflySettings, LeapSettings { @Default([]) List starred, @Default([]) List favoriteTemplates, @Default('') String defaultTemplate, + @Default(kDefaultFileName) String defaultFileName, @Default(NavigatorPosition.left) NavigatorPosition navigatorPosition, @Default(ToolbarPosition.inline) ToolbarPosition toolbarPosition, @Default(ToolbarSize.normal) ToolbarSize toolbarSize, @@ -636,6 +638,7 @@ sealed class ButterflySettings with _$ButterflySettings, LeapSettings { .toList() ?? [], defaultTemplate: prefs.getString('default_template') ?? '', + defaultFileName: prefs.getString('default_file_name') ?? kDefaultFileName, toolbarPosition: prefs.containsKey('toolbar_position') ? _enumByNameOr( ToolbarPosition.values, @@ -847,6 +850,7 @@ sealed class ButterflySettings with _$ButterflySettings, LeapSettings { ); await prefs.setInt('version', 0); await prefs.setString('default_template', defaultTemplate); + await prefs.setString('default_file_name', defaultFileName); await prefs.setString('toolbar_position', toolbarPosition.name); await prefs.setBool('navigation_rail', navigationRail); await prefs.setString('sort_by', sortBy.name); @@ -1413,6 +1417,17 @@ class SettingsCubit extends Cubit return save(); } + Future changeDefaultFileName(String pattern) { + emit( + state.copyWith( + defaultFileName: pattern.trim().isEmpty + ? kDefaultFileName + : pattern.trim(), + ), + ); + return save(); + } + Future toggleFavoriteTemplate(FavoriteLocation template) { final favorites = state.favoriteTemplates.toList(); if (favorites.contains(template)) { diff --git a/app/lib/cubits/settings.freezed.dart b/app/lib/cubits/settings.freezed.dart index c682f02266fd..f89280011ea3 100644 --- a/app/lib/cubits/settings.freezed.dart +++ b/app/lib/cubits/settings.freezed.dart @@ -710,7 +710,7 @@ as int, /// @nodoc mixin _$ButterflySettings implements DiagnosticableTreeMixin { - ThemeMode get theme; ThemeDensity get density; double? get limitViewportMultiplier; bool get limitViewportPositive; String get localeTag; String get documentPath; double get gestureSensitivity; double get touchSensitivity; double get selectSensitivity; double get scrollSensitivity; bool? get penOnlyInput; bool get showPenOnlyToggle; bool get inputGestures; String get design; BannerVisibility get bannerVisibility;@JsonKey(includeFromJson: false, includeToJson: false) List get history; bool get zoomEnabled; ZoomPosition get zoomPosition; ZoomPosition get propertyPosition; String? get lastVersion;@JsonKey(includeFromJson: false, includeToJson: false) List get connections; String get defaultRemote; bool get nativeTitleBar; bool get startInFullScreen; bool get navigationRail; IgnorePressure get ignorePressure; SyncMode get syncMode; InputConfiguration get inputConfiguration; String get fallbackPack; List get starred; List get favoriteTemplates; String get defaultTemplate; NavigatorPosition get navigatorPosition; ToolbarPosition get toolbarPosition; ToolbarSize get toolbarSize; SortBy get sortBy; SortOrder get sortOrder; double get imageScale; PlatformTheme get platformTheme;@SRGBConverter() List get recentColors; List get flags; bool get spreadPages; bool get highContrast; bool get gridView; bool get hideExtension; bool get autosave; bool get showSaveButton; int get toolbarRows; bool get delayedAutosave; int get autosaveDelaySeconds; bool get hideCursorWhileDrawing; StartupBehavior get onStartup; DocumentStatePersistenceSettings get documentStatePersistence; SimpleToolbarVisibility get simpleToolbarVisibility; OptionsPanelPosition get optionsPanelPosition; RenderResolution get renderResolution; bool get moveOnGesture; List get swamps; PackAssetLocation? get selectedPalette; bool get showVerboseLogs; bool get showThumbnails; bool get bringMovedElementsToFront; List get favoriteTools; + ThemeMode get theme; ThemeDensity get density; double? get limitViewportMultiplier; bool get limitViewportPositive; String get localeTag; String get documentPath; double get gestureSensitivity; double get touchSensitivity; double get selectSensitivity; double get scrollSensitivity; bool? get penOnlyInput; bool get showPenOnlyToggle; bool get inputGestures; String get design; BannerVisibility get bannerVisibility;@JsonKey(includeFromJson: false, includeToJson: false) List get history; bool get zoomEnabled; ZoomPosition get zoomPosition; ZoomPosition get propertyPosition; String? get lastVersion;@JsonKey(includeFromJson: false, includeToJson: false) List get connections; String get defaultRemote; bool get nativeTitleBar; bool get startInFullScreen; bool get navigationRail; IgnorePressure get ignorePressure; SyncMode get syncMode; InputConfiguration get inputConfiguration; String get fallbackPack; List get starred; List get favoriteTemplates; String get defaultTemplate; String get defaultFileName; NavigatorPosition get navigatorPosition; ToolbarPosition get toolbarPosition; ToolbarSize get toolbarSize; SortBy get sortBy; SortOrder get sortOrder; double get imageScale; PlatformTheme get platformTheme;@SRGBConverter() List get recentColors; List get flags; bool get spreadPages; bool get highContrast; bool get gridView; bool get hideExtension; bool get autosave; bool get showSaveButton; int get toolbarRows; bool get delayedAutosave; int get autosaveDelaySeconds; bool get hideCursorWhileDrawing; StartupBehavior get onStartup; DocumentStatePersistenceSettings get documentStatePersistence; SimpleToolbarVisibility get simpleToolbarVisibility; OptionsPanelPosition get optionsPanelPosition; RenderResolution get renderResolution; bool get moveOnGesture; List get swamps; PackAssetLocation? get selectedPalette; bool get showVerboseLogs; bool get showThumbnails; bool get bringMovedElementsToFront; List get favoriteTools; /// Create a copy of ButterflySettings /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -724,21 +724,21 @@ $ButterflySettingsCopyWith get copyWith => _$ButterflySetting void debugFillProperties(DiagnosticPropertiesBuilder properties) { properties ..add(DiagnosticsProperty('type', 'ButterflySettings')) - ..add(DiagnosticsProperty('theme', theme))..add(DiagnosticsProperty('density', density))..add(DiagnosticsProperty('limitViewportMultiplier', limitViewportMultiplier))..add(DiagnosticsProperty('limitViewportPositive', limitViewportPositive))..add(DiagnosticsProperty('localeTag', localeTag))..add(DiagnosticsProperty('documentPath', documentPath))..add(DiagnosticsProperty('gestureSensitivity', gestureSensitivity))..add(DiagnosticsProperty('touchSensitivity', touchSensitivity))..add(DiagnosticsProperty('selectSensitivity', selectSensitivity))..add(DiagnosticsProperty('scrollSensitivity', scrollSensitivity))..add(DiagnosticsProperty('penOnlyInput', penOnlyInput))..add(DiagnosticsProperty('showPenOnlyToggle', showPenOnlyToggle))..add(DiagnosticsProperty('inputGestures', inputGestures))..add(DiagnosticsProperty('design', design))..add(DiagnosticsProperty('bannerVisibility', bannerVisibility))..add(DiagnosticsProperty('history', history))..add(DiagnosticsProperty('zoomEnabled', zoomEnabled))..add(DiagnosticsProperty('zoomPosition', zoomPosition))..add(DiagnosticsProperty('propertyPosition', propertyPosition))..add(DiagnosticsProperty('lastVersion', lastVersion))..add(DiagnosticsProperty('connections', connections))..add(DiagnosticsProperty('defaultRemote', defaultRemote))..add(DiagnosticsProperty('nativeTitleBar', nativeTitleBar))..add(DiagnosticsProperty('startInFullScreen', startInFullScreen))..add(DiagnosticsProperty('navigationRail', navigationRail))..add(DiagnosticsProperty('ignorePressure', ignorePressure))..add(DiagnosticsProperty('syncMode', syncMode))..add(DiagnosticsProperty('inputConfiguration', inputConfiguration))..add(DiagnosticsProperty('fallbackPack', fallbackPack))..add(DiagnosticsProperty('starred', starred))..add(DiagnosticsProperty('favoriteTemplates', favoriteTemplates))..add(DiagnosticsProperty('defaultTemplate', defaultTemplate))..add(DiagnosticsProperty('navigatorPosition', navigatorPosition))..add(DiagnosticsProperty('toolbarPosition', toolbarPosition))..add(DiagnosticsProperty('toolbarSize', toolbarSize))..add(DiagnosticsProperty('sortBy', sortBy))..add(DiagnosticsProperty('sortOrder', sortOrder))..add(DiagnosticsProperty('imageScale', imageScale))..add(DiagnosticsProperty('platformTheme', platformTheme))..add(DiagnosticsProperty('recentColors', recentColors))..add(DiagnosticsProperty('flags', flags))..add(DiagnosticsProperty('spreadPages', spreadPages))..add(DiagnosticsProperty('highContrast', highContrast))..add(DiagnosticsProperty('gridView', gridView))..add(DiagnosticsProperty('hideExtension', hideExtension))..add(DiagnosticsProperty('autosave', autosave))..add(DiagnosticsProperty('showSaveButton', showSaveButton))..add(DiagnosticsProperty('toolbarRows', toolbarRows))..add(DiagnosticsProperty('delayedAutosave', delayedAutosave))..add(DiagnosticsProperty('autosaveDelaySeconds', autosaveDelaySeconds))..add(DiagnosticsProperty('hideCursorWhileDrawing', hideCursorWhileDrawing))..add(DiagnosticsProperty('onStartup', onStartup))..add(DiagnosticsProperty('documentStatePersistence', documentStatePersistence))..add(DiagnosticsProperty('simpleToolbarVisibility', simpleToolbarVisibility))..add(DiagnosticsProperty('optionsPanelPosition', optionsPanelPosition))..add(DiagnosticsProperty('renderResolution', renderResolution))..add(DiagnosticsProperty('moveOnGesture', moveOnGesture))..add(DiagnosticsProperty('swamps', swamps))..add(DiagnosticsProperty('selectedPalette', selectedPalette))..add(DiagnosticsProperty('showVerboseLogs', showVerboseLogs))..add(DiagnosticsProperty('showThumbnails', showThumbnails))..add(DiagnosticsProperty('bringMovedElementsToFront', bringMovedElementsToFront))..add(DiagnosticsProperty('favoriteTools', favoriteTools)); + ..add(DiagnosticsProperty('theme', theme))..add(DiagnosticsProperty('density', density))..add(DiagnosticsProperty('limitViewportMultiplier', limitViewportMultiplier))..add(DiagnosticsProperty('limitViewportPositive', limitViewportPositive))..add(DiagnosticsProperty('localeTag', localeTag))..add(DiagnosticsProperty('documentPath', documentPath))..add(DiagnosticsProperty('gestureSensitivity', gestureSensitivity))..add(DiagnosticsProperty('touchSensitivity', touchSensitivity))..add(DiagnosticsProperty('selectSensitivity', selectSensitivity))..add(DiagnosticsProperty('scrollSensitivity', scrollSensitivity))..add(DiagnosticsProperty('penOnlyInput', penOnlyInput))..add(DiagnosticsProperty('showPenOnlyToggle', showPenOnlyToggle))..add(DiagnosticsProperty('inputGestures', inputGestures))..add(DiagnosticsProperty('design', design))..add(DiagnosticsProperty('bannerVisibility', bannerVisibility))..add(DiagnosticsProperty('history', history))..add(DiagnosticsProperty('zoomEnabled', zoomEnabled))..add(DiagnosticsProperty('zoomPosition', zoomPosition))..add(DiagnosticsProperty('propertyPosition', propertyPosition))..add(DiagnosticsProperty('lastVersion', lastVersion))..add(DiagnosticsProperty('connections', connections))..add(DiagnosticsProperty('defaultRemote', defaultRemote))..add(DiagnosticsProperty('nativeTitleBar', nativeTitleBar))..add(DiagnosticsProperty('startInFullScreen', startInFullScreen))..add(DiagnosticsProperty('navigationRail', navigationRail))..add(DiagnosticsProperty('ignorePressure', ignorePressure))..add(DiagnosticsProperty('syncMode', syncMode))..add(DiagnosticsProperty('inputConfiguration', inputConfiguration))..add(DiagnosticsProperty('fallbackPack', fallbackPack))..add(DiagnosticsProperty('starred', starred))..add(DiagnosticsProperty('favoriteTemplates', favoriteTemplates))..add(DiagnosticsProperty('defaultTemplate', defaultTemplate))..add(DiagnosticsProperty('defaultFileName', defaultFileName))..add(DiagnosticsProperty('navigatorPosition', navigatorPosition))..add(DiagnosticsProperty('toolbarPosition', toolbarPosition))..add(DiagnosticsProperty('toolbarSize', toolbarSize))..add(DiagnosticsProperty('sortBy', sortBy))..add(DiagnosticsProperty('sortOrder', sortOrder))..add(DiagnosticsProperty('imageScale', imageScale))..add(DiagnosticsProperty('platformTheme', platformTheme))..add(DiagnosticsProperty('recentColors', recentColors))..add(DiagnosticsProperty('flags', flags))..add(DiagnosticsProperty('spreadPages', spreadPages))..add(DiagnosticsProperty('highContrast', highContrast))..add(DiagnosticsProperty('gridView', gridView))..add(DiagnosticsProperty('hideExtension', hideExtension))..add(DiagnosticsProperty('autosave', autosave))..add(DiagnosticsProperty('showSaveButton', showSaveButton))..add(DiagnosticsProperty('toolbarRows', toolbarRows))..add(DiagnosticsProperty('delayedAutosave', delayedAutosave))..add(DiagnosticsProperty('autosaveDelaySeconds', autosaveDelaySeconds))..add(DiagnosticsProperty('hideCursorWhileDrawing', hideCursorWhileDrawing))..add(DiagnosticsProperty('onStartup', onStartup))..add(DiagnosticsProperty('documentStatePersistence', documentStatePersistence))..add(DiagnosticsProperty('simpleToolbarVisibility', simpleToolbarVisibility))..add(DiagnosticsProperty('optionsPanelPosition', optionsPanelPosition))..add(DiagnosticsProperty('renderResolution', renderResolution))..add(DiagnosticsProperty('moveOnGesture', moveOnGesture))..add(DiagnosticsProperty('swamps', swamps))..add(DiagnosticsProperty('selectedPalette', selectedPalette))..add(DiagnosticsProperty('showVerboseLogs', showVerboseLogs))..add(DiagnosticsProperty('showThumbnails', showThumbnails))..add(DiagnosticsProperty('bringMovedElementsToFront', bringMovedElementsToFront))..add(DiagnosticsProperty('favoriteTools', favoriteTools)); } @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ButterflySettings&&(identical(other.theme, theme) || other.theme == theme)&&(identical(other.density, density) || other.density == density)&&(identical(other.limitViewportMultiplier, limitViewportMultiplier) || other.limitViewportMultiplier == limitViewportMultiplier)&&(identical(other.limitViewportPositive, limitViewportPositive) || other.limitViewportPositive == limitViewportPositive)&&(identical(other.localeTag, localeTag) || other.localeTag == localeTag)&&(identical(other.documentPath, documentPath) || other.documentPath == documentPath)&&(identical(other.gestureSensitivity, gestureSensitivity) || other.gestureSensitivity == gestureSensitivity)&&(identical(other.touchSensitivity, touchSensitivity) || other.touchSensitivity == touchSensitivity)&&(identical(other.selectSensitivity, selectSensitivity) || other.selectSensitivity == selectSensitivity)&&(identical(other.scrollSensitivity, scrollSensitivity) || other.scrollSensitivity == scrollSensitivity)&&(identical(other.penOnlyInput, penOnlyInput) || other.penOnlyInput == penOnlyInput)&&(identical(other.showPenOnlyToggle, showPenOnlyToggle) || other.showPenOnlyToggle == showPenOnlyToggle)&&(identical(other.inputGestures, inputGestures) || other.inputGestures == inputGestures)&&(identical(other.design, design) || other.design == design)&&(identical(other.bannerVisibility, bannerVisibility) || other.bannerVisibility == bannerVisibility)&&const DeepCollectionEquality().equals(other.history, history)&&(identical(other.zoomEnabled, zoomEnabled) || other.zoomEnabled == zoomEnabled)&&(identical(other.zoomPosition, zoomPosition) || other.zoomPosition == zoomPosition)&&(identical(other.propertyPosition, propertyPosition) || other.propertyPosition == propertyPosition)&&(identical(other.lastVersion, lastVersion) || other.lastVersion == lastVersion)&&const DeepCollectionEquality().equals(other.connections, connections)&&(identical(other.defaultRemote, defaultRemote) || other.defaultRemote == defaultRemote)&&(identical(other.nativeTitleBar, nativeTitleBar) || other.nativeTitleBar == nativeTitleBar)&&(identical(other.startInFullScreen, startInFullScreen) || other.startInFullScreen == startInFullScreen)&&(identical(other.navigationRail, navigationRail) || other.navigationRail == navigationRail)&&(identical(other.ignorePressure, ignorePressure) || other.ignorePressure == ignorePressure)&&(identical(other.syncMode, syncMode) || other.syncMode == syncMode)&&(identical(other.inputConfiguration, inputConfiguration) || other.inputConfiguration == inputConfiguration)&&(identical(other.fallbackPack, fallbackPack) || other.fallbackPack == fallbackPack)&&const DeepCollectionEquality().equals(other.starred, starred)&&const DeepCollectionEquality().equals(other.favoriteTemplates, favoriteTemplates)&&(identical(other.defaultTemplate, defaultTemplate) || other.defaultTemplate == defaultTemplate)&&(identical(other.navigatorPosition, navigatorPosition) || other.navigatorPosition == navigatorPosition)&&(identical(other.toolbarPosition, toolbarPosition) || other.toolbarPosition == toolbarPosition)&&(identical(other.toolbarSize, toolbarSize) || other.toolbarSize == toolbarSize)&&(identical(other.sortBy, sortBy) || other.sortBy == sortBy)&&(identical(other.sortOrder, sortOrder) || other.sortOrder == sortOrder)&&(identical(other.imageScale, imageScale) || other.imageScale == imageScale)&&(identical(other.platformTheme, platformTheme) || other.platformTheme == platformTheme)&&const DeepCollectionEquality().equals(other.recentColors, recentColors)&&const DeepCollectionEquality().equals(other.flags, flags)&&(identical(other.spreadPages, spreadPages) || other.spreadPages == spreadPages)&&(identical(other.highContrast, highContrast) || other.highContrast == highContrast)&&(identical(other.gridView, gridView) || other.gridView == gridView)&&(identical(other.hideExtension, hideExtension) || other.hideExtension == hideExtension)&&(identical(other.autosave, autosave) || other.autosave == autosave)&&(identical(other.showSaveButton, showSaveButton) || other.showSaveButton == showSaveButton)&&(identical(other.toolbarRows, toolbarRows) || other.toolbarRows == toolbarRows)&&(identical(other.delayedAutosave, delayedAutosave) || other.delayedAutosave == delayedAutosave)&&(identical(other.autosaveDelaySeconds, autosaveDelaySeconds) || other.autosaveDelaySeconds == autosaveDelaySeconds)&&(identical(other.hideCursorWhileDrawing, hideCursorWhileDrawing) || other.hideCursorWhileDrawing == hideCursorWhileDrawing)&&(identical(other.onStartup, onStartup) || other.onStartup == onStartup)&&(identical(other.documentStatePersistence, documentStatePersistence) || other.documentStatePersistence == documentStatePersistence)&&(identical(other.simpleToolbarVisibility, simpleToolbarVisibility) || other.simpleToolbarVisibility == simpleToolbarVisibility)&&(identical(other.optionsPanelPosition, optionsPanelPosition) || other.optionsPanelPosition == optionsPanelPosition)&&(identical(other.renderResolution, renderResolution) || other.renderResolution == renderResolution)&&(identical(other.moveOnGesture, moveOnGesture) || other.moveOnGesture == moveOnGesture)&&const DeepCollectionEquality().equals(other.swamps, swamps)&&(identical(other.selectedPalette, selectedPalette) || other.selectedPalette == selectedPalette)&&(identical(other.showVerboseLogs, showVerboseLogs) || other.showVerboseLogs == showVerboseLogs)&&(identical(other.showThumbnails, showThumbnails) || other.showThumbnails == showThumbnails)&&(identical(other.bringMovedElementsToFront, bringMovedElementsToFront) || other.bringMovedElementsToFront == bringMovedElementsToFront)&&const DeepCollectionEquality().equals(other.favoriteTools, favoriteTools)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is ButterflySettings&&(identical(other.theme, theme) || other.theme == theme)&&(identical(other.density, density) || other.density == density)&&(identical(other.limitViewportMultiplier, limitViewportMultiplier) || other.limitViewportMultiplier == limitViewportMultiplier)&&(identical(other.limitViewportPositive, limitViewportPositive) || other.limitViewportPositive == limitViewportPositive)&&(identical(other.localeTag, localeTag) || other.localeTag == localeTag)&&(identical(other.documentPath, documentPath) || other.documentPath == documentPath)&&(identical(other.gestureSensitivity, gestureSensitivity) || other.gestureSensitivity == gestureSensitivity)&&(identical(other.touchSensitivity, touchSensitivity) || other.touchSensitivity == touchSensitivity)&&(identical(other.selectSensitivity, selectSensitivity) || other.selectSensitivity == selectSensitivity)&&(identical(other.scrollSensitivity, scrollSensitivity) || other.scrollSensitivity == scrollSensitivity)&&(identical(other.penOnlyInput, penOnlyInput) || other.penOnlyInput == penOnlyInput)&&(identical(other.showPenOnlyToggle, showPenOnlyToggle) || other.showPenOnlyToggle == showPenOnlyToggle)&&(identical(other.inputGestures, inputGestures) || other.inputGestures == inputGestures)&&(identical(other.design, design) || other.design == design)&&(identical(other.bannerVisibility, bannerVisibility) || other.bannerVisibility == bannerVisibility)&&const DeepCollectionEquality().equals(other.history, history)&&(identical(other.zoomEnabled, zoomEnabled) || other.zoomEnabled == zoomEnabled)&&(identical(other.zoomPosition, zoomPosition) || other.zoomPosition == zoomPosition)&&(identical(other.propertyPosition, propertyPosition) || other.propertyPosition == propertyPosition)&&(identical(other.lastVersion, lastVersion) || other.lastVersion == lastVersion)&&const DeepCollectionEquality().equals(other.connections, connections)&&(identical(other.defaultRemote, defaultRemote) || other.defaultRemote == defaultRemote)&&(identical(other.nativeTitleBar, nativeTitleBar) || other.nativeTitleBar == nativeTitleBar)&&(identical(other.startInFullScreen, startInFullScreen) || other.startInFullScreen == startInFullScreen)&&(identical(other.navigationRail, navigationRail) || other.navigationRail == navigationRail)&&(identical(other.ignorePressure, ignorePressure) || other.ignorePressure == ignorePressure)&&(identical(other.syncMode, syncMode) || other.syncMode == syncMode)&&(identical(other.inputConfiguration, inputConfiguration) || other.inputConfiguration == inputConfiguration)&&(identical(other.fallbackPack, fallbackPack) || other.fallbackPack == fallbackPack)&&const DeepCollectionEquality().equals(other.starred, starred)&&const DeepCollectionEquality().equals(other.favoriteTemplates, favoriteTemplates)&&(identical(other.defaultTemplate, defaultTemplate) || other.defaultTemplate == defaultTemplate)&&(identical(other.defaultFileName, defaultFileName) || other.defaultFileName == defaultFileName)&&(identical(other.navigatorPosition, navigatorPosition) || other.navigatorPosition == navigatorPosition)&&(identical(other.toolbarPosition, toolbarPosition) || other.toolbarPosition == toolbarPosition)&&(identical(other.toolbarSize, toolbarSize) || other.toolbarSize == toolbarSize)&&(identical(other.sortBy, sortBy) || other.sortBy == sortBy)&&(identical(other.sortOrder, sortOrder) || other.sortOrder == sortOrder)&&(identical(other.imageScale, imageScale) || other.imageScale == imageScale)&&(identical(other.platformTheme, platformTheme) || other.platformTheme == platformTheme)&&const DeepCollectionEquality().equals(other.recentColors, recentColors)&&const DeepCollectionEquality().equals(other.flags, flags)&&(identical(other.spreadPages, spreadPages) || other.spreadPages == spreadPages)&&(identical(other.highContrast, highContrast) || other.highContrast == highContrast)&&(identical(other.gridView, gridView) || other.gridView == gridView)&&(identical(other.hideExtension, hideExtension) || other.hideExtension == hideExtension)&&(identical(other.autosave, autosave) || other.autosave == autosave)&&(identical(other.showSaveButton, showSaveButton) || other.showSaveButton == showSaveButton)&&(identical(other.toolbarRows, toolbarRows) || other.toolbarRows == toolbarRows)&&(identical(other.delayedAutosave, delayedAutosave) || other.delayedAutosave == delayedAutosave)&&(identical(other.autosaveDelaySeconds, autosaveDelaySeconds) || other.autosaveDelaySeconds == autosaveDelaySeconds)&&(identical(other.hideCursorWhileDrawing, hideCursorWhileDrawing) || other.hideCursorWhileDrawing == hideCursorWhileDrawing)&&(identical(other.onStartup, onStartup) || other.onStartup == onStartup)&&(identical(other.documentStatePersistence, documentStatePersistence) || other.documentStatePersistence == documentStatePersistence)&&(identical(other.simpleToolbarVisibility, simpleToolbarVisibility) || other.simpleToolbarVisibility == simpleToolbarVisibility)&&(identical(other.optionsPanelPosition, optionsPanelPosition) || other.optionsPanelPosition == optionsPanelPosition)&&(identical(other.renderResolution, renderResolution) || other.renderResolution == renderResolution)&&(identical(other.moveOnGesture, moveOnGesture) || other.moveOnGesture == moveOnGesture)&&const DeepCollectionEquality().equals(other.swamps, swamps)&&(identical(other.selectedPalette, selectedPalette) || other.selectedPalette == selectedPalette)&&(identical(other.showVerboseLogs, showVerboseLogs) || other.showVerboseLogs == showVerboseLogs)&&(identical(other.showThumbnails, showThumbnails) || other.showThumbnails == showThumbnails)&&(identical(other.bringMovedElementsToFront, bringMovedElementsToFront) || other.bringMovedElementsToFront == bringMovedElementsToFront)&&const DeepCollectionEquality().equals(other.favoriteTools, favoriteTools)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hashAll([runtimeType,theme,density,limitViewportMultiplier,limitViewportPositive,localeTag,documentPath,gestureSensitivity,touchSensitivity,selectSensitivity,scrollSensitivity,penOnlyInput,showPenOnlyToggle,inputGestures,design,bannerVisibility,const DeepCollectionEquality().hash(history),zoomEnabled,zoomPosition,propertyPosition,lastVersion,const DeepCollectionEquality().hash(connections),defaultRemote,nativeTitleBar,startInFullScreen,navigationRail,ignorePressure,syncMode,inputConfiguration,fallbackPack,const DeepCollectionEquality().hash(starred),const DeepCollectionEquality().hash(favoriteTemplates),defaultTemplate,navigatorPosition,toolbarPosition,toolbarSize,sortBy,sortOrder,imageScale,platformTheme,const DeepCollectionEquality().hash(recentColors),const DeepCollectionEquality().hash(flags),spreadPages,highContrast,gridView,hideExtension,autosave,showSaveButton,toolbarRows,delayedAutosave,autosaveDelaySeconds,hideCursorWhileDrawing,onStartup,documentStatePersistence,simpleToolbarVisibility,optionsPanelPosition,renderResolution,moveOnGesture,const DeepCollectionEquality().hash(swamps),selectedPalette,showVerboseLogs,showThumbnails,bringMovedElementsToFront,const DeepCollectionEquality().hash(favoriteTools)]); +int get hashCode => Object.hashAll([runtimeType,theme,density,limitViewportMultiplier,limitViewportPositive,localeTag,documentPath,gestureSensitivity,touchSensitivity,selectSensitivity,scrollSensitivity,penOnlyInput,showPenOnlyToggle,inputGestures,design,bannerVisibility,const DeepCollectionEquality().hash(history),zoomEnabled,zoomPosition,propertyPosition,lastVersion,const DeepCollectionEquality().hash(connections),defaultRemote,nativeTitleBar,startInFullScreen,navigationRail,ignorePressure,syncMode,inputConfiguration,fallbackPack,const DeepCollectionEquality().hash(starred),const DeepCollectionEquality().hash(favoriteTemplates),defaultTemplate,defaultFileName,navigatorPosition,toolbarPosition,toolbarSize,sortBy,sortOrder,imageScale,platformTheme,const DeepCollectionEquality().hash(recentColors),const DeepCollectionEquality().hash(flags),spreadPages,highContrast,gridView,hideExtension,autosave,showSaveButton,toolbarRows,delayedAutosave,autosaveDelaySeconds,hideCursorWhileDrawing,onStartup,documentStatePersistence,simpleToolbarVisibility,optionsPanelPosition,renderResolution,moveOnGesture,const DeepCollectionEquality().hash(swamps),selectedPalette,showVerboseLogs,showThumbnails,bringMovedElementsToFront,const DeepCollectionEquality().hash(favoriteTools)]); @override String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'ButterflySettings(theme: $theme, density: $density, limitViewportMultiplier: $limitViewportMultiplier, limitViewportPositive: $limitViewportPositive, localeTag: $localeTag, documentPath: $documentPath, gestureSensitivity: $gestureSensitivity, touchSensitivity: $touchSensitivity, selectSensitivity: $selectSensitivity, scrollSensitivity: $scrollSensitivity, penOnlyInput: $penOnlyInput, showPenOnlyToggle: $showPenOnlyToggle, inputGestures: $inputGestures, design: $design, bannerVisibility: $bannerVisibility, history: $history, zoomEnabled: $zoomEnabled, zoomPosition: $zoomPosition, propertyPosition: $propertyPosition, lastVersion: $lastVersion, connections: $connections, defaultRemote: $defaultRemote, nativeTitleBar: $nativeTitleBar, startInFullScreen: $startInFullScreen, navigationRail: $navigationRail, ignorePressure: $ignorePressure, syncMode: $syncMode, inputConfiguration: $inputConfiguration, fallbackPack: $fallbackPack, starred: $starred, favoriteTemplates: $favoriteTemplates, defaultTemplate: $defaultTemplate, navigatorPosition: $navigatorPosition, toolbarPosition: $toolbarPosition, toolbarSize: $toolbarSize, sortBy: $sortBy, sortOrder: $sortOrder, imageScale: $imageScale, platformTheme: $platformTheme, recentColors: $recentColors, flags: $flags, spreadPages: $spreadPages, highContrast: $highContrast, gridView: $gridView, hideExtension: $hideExtension, autosave: $autosave, showSaveButton: $showSaveButton, toolbarRows: $toolbarRows, delayedAutosave: $delayedAutosave, autosaveDelaySeconds: $autosaveDelaySeconds, hideCursorWhileDrawing: $hideCursorWhileDrawing, onStartup: $onStartup, documentStatePersistence: $documentStatePersistence, simpleToolbarVisibility: $simpleToolbarVisibility, optionsPanelPosition: $optionsPanelPosition, renderResolution: $renderResolution, moveOnGesture: $moveOnGesture, swamps: $swamps, selectedPalette: $selectedPalette, showVerboseLogs: $showVerboseLogs, showThumbnails: $showThumbnails, bringMovedElementsToFront: $bringMovedElementsToFront, favoriteTools: $favoriteTools)'; + return 'ButterflySettings(theme: $theme, density: $density, limitViewportMultiplier: $limitViewportMultiplier, limitViewportPositive: $limitViewportPositive, localeTag: $localeTag, documentPath: $documentPath, gestureSensitivity: $gestureSensitivity, touchSensitivity: $touchSensitivity, selectSensitivity: $selectSensitivity, scrollSensitivity: $scrollSensitivity, penOnlyInput: $penOnlyInput, showPenOnlyToggle: $showPenOnlyToggle, inputGestures: $inputGestures, design: $design, bannerVisibility: $bannerVisibility, history: $history, zoomEnabled: $zoomEnabled, zoomPosition: $zoomPosition, propertyPosition: $propertyPosition, lastVersion: $lastVersion, connections: $connections, defaultRemote: $defaultRemote, nativeTitleBar: $nativeTitleBar, startInFullScreen: $startInFullScreen, navigationRail: $navigationRail, ignorePressure: $ignorePressure, syncMode: $syncMode, inputConfiguration: $inputConfiguration, fallbackPack: $fallbackPack, starred: $starred, favoriteTemplates: $favoriteTemplates, defaultTemplate: $defaultTemplate, defaultFileName: $defaultFileName, navigatorPosition: $navigatorPosition, toolbarPosition: $toolbarPosition, toolbarSize: $toolbarSize, sortBy: $sortBy, sortOrder: $sortOrder, imageScale: $imageScale, platformTheme: $platformTheme, recentColors: $recentColors, flags: $flags, spreadPages: $spreadPages, highContrast: $highContrast, gridView: $gridView, hideExtension: $hideExtension, autosave: $autosave, showSaveButton: $showSaveButton, toolbarRows: $toolbarRows, delayedAutosave: $delayedAutosave, autosaveDelaySeconds: $autosaveDelaySeconds, hideCursorWhileDrawing: $hideCursorWhileDrawing, onStartup: $onStartup, documentStatePersistence: $documentStatePersistence, simpleToolbarVisibility: $simpleToolbarVisibility, optionsPanelPosition: $optionsPanelPosition, renderResolution: $renderResolution, moveOnGesture: $moveOnGesture, swamps: $swamps, selectedPalette: $selectedPalette, showVerboseLogs: $showVerboseLogs, showThumbnails: $showThumbnails, bringMovedElementsToFront: $bringMovedElementsToFront, favoriteTools: $favoriteTools)'; } @@ -749,7 +749,7 @@ abstract mixin class $ButterflySettingsCopyWith<$Res> { factory $ButterflySettingsCopyWith(ButterflySettings value, $Res Function(ButterflySettings) _then) = _$ButterflySettingsCopyWithImpl; @useResult $Res call({ - ThemeMode theme, ThemeDensity density, double? limitViewportMultiplier, bool limitViewportPositive, String localeTag, String documentPath, double gestureSensitivity, double touchSensitivity, double selectSensitivity, double scrollSensitivity, bool? penOnlyInput, bool showPenOnlyToggle, bool inputGestures, String design, BannerVisibility bannerVisibility,@JsonKey(includeFromJson: false, includeToJson: false) List history, bool zoomEnabled, ZoomPosition zoomPosition, ZoomPosition propertyPosition, String? lastVersion,@JsonKey(includeFromJson: false, includeToJson: false) List connections, String defaultRemote, bool nativeTitleBar, bool startInFullScreen, bool navigationRail, IgnorePressure ignorePressure, SyncMode syncMode, InputConfiguration inputConfiguration, String fallbackPack, List starred, List favoriteTemplates, String defaultTemplate, NavigatorPosition navigatorPosition, ToolbarPosition toolbarPosition, ToolbarSize toolbarSize, SortBy sortBy, SortOrder sortOrder, double imageScale, PlatformTheme platformTheme,@SRGBConverter() List recentColors, List flags, bool spreadPages, bool highContrast, bool gridView, bool hideExtension, bool autosave, bool showSaveButton, int toolbarRows, bool delayedAutosave, int autosaveDelaySeconds, bool hideCursorWhileDrawing, StartupBehavior onStartup, DocumentStatePersistenceSettings documentStatePersistence, SimpleToolbarVisibility simpleToolbarVisibility, OptionsPanelPosition optionsPanelPosition, RenderResolution renderResolution, bool moveOnGesture, List swamps, PackAssetLocation? selectedPalette, bool showVerboseLogs, bool showThumbnails, bool bringMovedElementsToFront, List favoriteTools + ThemeMode theme, ThemeDensity density, double? limitViewportMultiplier, bool limitViewportPositive, String localeTag, String documentPath, double gestureSensitivity, double touchSensitivity, double selectSensitivity, double scrollSensitivity, bool? penOnlyInput, bool showPenOnlyToggle, bool inputGestures, String design, BannerVisibility bannerVisibility,@JsonKey(includeFromJson: false, includeToJson: false) List history, bool zoomEnabled, ZoomPosition zoomPosition, ZoomPosition propertyPosition, String? lastVersion,@JsonKey(includeFromJson: false, includeToJson: false) List connections, String defaultRemote, bool nativeTitleBar, bool startInFullScreen, bool navigationRail, IgnorePressure ignorePressure, SyncMode syncMode, InputConfiguration inputConfiguration, String fallbackPack, List starred, List favoriteTemplates, String defaultTemplate, String defaultFileName, NavigatorPosition navigatorPosition, ToolbarPosition toolbarPosition, ToolbarSize toolbarSize, SortBy sortBy, SortOrder sortOrder, double imageScale, PlatformTheme platformTheme,@SRGBConverter() List recentColors, List flags, bool spreadPages, bool highContrast, bool gridView, bool hideExtension, bool autosave, bool showSaveButton, int toolbarRows, bool delayedAutosave, int autosaveDelaySeconds, bool hideCursorWhileDrawing, StartupBehavior onStartup, DocumentStatePersistenceSettings documentStatePersistence, SimpleToolbarVisibility simpleToolbarVisibility, OptionsPanelPosition optionsPanelPosition, RenderResolution renderResolution, bool moveOnGesture, List swamps, PackAssetLocation? selectedPalette, bool showVerboseLogs, bool showThumbnails, bool bringMovedElementsToFront, List favoriteTools }); @@ -766,7 +766,7 @@ class _$ButterflySettingsCopyWithImpl<$Res> /// Create a copy of ButterflySettings /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? theme = null,Object? density = null,Object? limitViewportMultiplier = freezed,Object? limitViewportPositive = null,Object? localeTag = null,Object? documentPath = null,Object? gestureSensitivity = null,Object? touchSensitivity = null,Object? selectSensitivity = null,Object? scrollSensitivity = null,Object? penOnlyInput = freezed,Object? showPenOnlyToggle = null,Object? inputGestures = null,Object? design = null,Object? bannerVisibility = null,Object? history = null,Object? zoomEnabled = null,Object? zoomPosition = null,Object? propertyPosition = null,Object? lastVersion = freezed,Object? connections = null,Object? defaultRemote = null,Object? nativeTitleBar = null,Object? startInFullScreen = null,Object? navigationRail = null,Object? ignorePressure = null,Object? syncMode = null,Object? inputConfiguration = null,Object? fallbackPack = null,Object? starred = null,Object? favoriteTemplates = null,Object? defaultTemplate = null,Object? navigatorPosition = null,Object? toolbarPosition = null,Object? toolbarSize = null,Object? sortBy = null,Object? sortOrder = null,Object? imageScale = null,Object? platformTheme = null,Object? recentColors = null,Object? flags = null,Object? spreadPages = null,Object? highContrast = null,Object? gridView = null,Object? hideExtension = null,Object? autosave = null,Object? showSaveButton = null,Object? toolbarRows = null,Object? delayedAutosave = null,Object? autosaveDelaySeconds = null,Object? hideCursorWhileDrawing = null,Object? onStartup = null,Object? documentStatePersistence = null,Object? simpleToolbarVisibility = null,Object? optionsPanelPosition = null,Object? renderResolution = null,Object? moveOnGesture = null,Object? swamps = null,Object? selectedPalette = freezed,Object? showVerboseLogs = null,Object? showThumbnails = null,Object? bringMovedElementsToFront = null,Object? favoriteTools = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? theme = null,Object? density = null,Object? limitViewportMultiplier = freezed,Object? limitViewportPositive = null,Object? localeTag = null,Object? documentPath = null,Object? gestureSensitivity = null,Object? touchSensitivity = null,Object? selectSensitivity = null,Object? scrollSensitivity = null,Object? penOnlyInput = freezed,Object? showPenOnlyToggle = null,Object? inputGestures = null,Object? design = null,Object? bannerVisibility = null,Object? history = null,Object? zoomEnabled = null,Object? zoomPosition = null,Object? propertyPosition = null,Object? lastVersion = freezed,Object? connections = null,Object? defaultRemote = null,Object? nativeTitleBar = null,Object? startInFullScreen = null,Object? navigationRail = null,Object? ignorePressure = null,Object? syncMode = null,Object? inputConfiguration = null,Object? fallbackPack = null,Object? starred = null,Object? favoriteTemplates = null,Object? defaultTemplate = null,Object? defaultFileName = null,Object? navigatorPosition = null,Object? toolbarPosition = null,Object? toolbarSize = null,Object? sortBy = null,Object? sortOrder = null,Object? imageScale = null,Object? platformTheme = null,Object? recentColors = null,Object? flags = null,Object? spreadPages = null,Object? highContrast = null,Object? gridView = null,Object? hideExtension = null,Object? autosave = null,Object? showSaveButton = null,Object? toolbarRows = null,Object? delayedAutosave = null,Object? autosaveDelaySeconds = null,Object? hideCursorWhileDrawing = null,Object? onStartup = null,Object? documentStatePersistence = null,Object? simpleToolbarVisibility = null,Object? optionsPanelPosition = null,Object? renderResolution = null,Object? moveOnGesture = null,Object? swamps = null,Object? selectedPalette = freezed,Object? showVerboseLogs = null,Object? showThumbnails = null,Object? bringMovedElementsToFront = null,Object? favoriteTools = null,}) { return _then(_self.copyWith( theme: null == theme ? _self.theme : theme // ignore: cast_nullable_to_non_nullable as ThemeMode,density: null == density ? _self.density : density // ignore: cast_nullable_to_non_nullable @@ -800,6 +800,7 @@ as InputConfiguration,fallbackPack: null == fallbackPack ? _self.fallbackPack : as String,starred: null == starred ? _self.starred : starred // ignore: cast_nullable_to_non_nullable as List,favoriteTemplates: null == favoriteTemplates ? _self.favoriteTemplates : favoriteTemplates // ignore: cast_nullable_to_non_nullable as List,defaultTemplate: null == defaultTemplate ? _self.defaultTemplate : defaultTemplate // ignore: cast_nullable_to_non_nullable +as String,defaultFileName: null == defaultFileName ? _self.defaultFileName : defaultFileName // ignore: cast_nullable_to_non_nullable as String,navigatorPosition: null == navigatorPosition ? _self.navigatorPosition : navigatorPosition // ignore: cast_nullable_to_non_nullable as NavigatorPosition,toolbarPosition: null == toolbarPosition ? _self.toolbarPosition : toolbarPosition // ignore: cast_nullable_to_non_nullable as ToolbarPosition,toolbarSize: null == toolbarSize ? _self.toolbarSize : toolbarSize // ignore: cast_nullable_to_non_nullable @@ -873,7 +874,7 @@ $PackAssetLocationCopyWith<$Res>? get selectedPalette { @JsonSerializable() class _ButterflySettings extends ButterflySettings with DiagnosticableTreeMixin { - const _ButterflySettings({this.theme = ThemeMode.system, this.density = ThemeDensity.system, this.limitViewportMultiplier, this.limitViewportPositive = false, this.localeTag = '', this.documentPath = '', this.gestureSensitivity = 1, this.touchSensitivity = 1, this.selectSensitivity = 1, this.scrollSensitivity = 1, this.penOnlyInput, this.showPenOnlyToggle = true, this.inputGestures = true, this.design = '', this.bannerVisibility = BannerVisibility.always, @JsonKey(includeFromJson: false, includeToJson: false) final List history = const [], this.zoomEnabled = true, this.zoomPosition = ZoomPosition.bottomRight, this.propertyPosition = ZoomPosition.topRight, this.lastVersion, @JsonKey(includeFromJson: false, includeToJson: false) final List connections = const [], this.defaultRemote = '', this.nativeTitleBar = false, this.startInFullScreen = false, this.navigationRail = true, this.ignorePressure = IgnorePressure.first, this.syncMode = SyncMode.noMobile, this.inputConfiguration = const InputConfiguration(), this.fallbackPack = '', final List starred = const [], final List favoriteTemplates = const [], this.defaultTemplate = '', this.navigatorPosition = NavigatorPosition.left, this.toolbarPosition = ToolbarPosition.inline, this.toolbarSize = ToolbarSize.normal, this.sortBy = SortBy.modified, this.sortOrder = SortOrder.descending, this.imageScale = 0.5, this.platformTheme = PlatformTheme.system, @SRGBConverter() final List recentColors = const [], final List flags = const [], this.spreadPages = false, this.highContrast = false, this.gridView = false, this.hideExtension = true, this.autosave = true, this.showSaveButton = true, this.toolbarRows = 1, this.delayedAutosave = true, this.autosaveDelaySeconds = 3, this.hideCursorWhileDrawing = false, this.onStartup = StartupBehavior.openHomeScreen, this.documentStatePersistence = const DocumentStatePersistenceSettings(), this.simpleToolbarVisibility = SimpleToolbarVisibility.show, this.optionsPanelPosition = OptionsPanelPosition.top, this.renderResolution = RenderResolution.normal, this.moveOnGesture = true, final List swamps = const [], this.selectedPalette, this.showVerboseLogs = false, this.showThumbnails = true, this.bringMovedElementsToFront = false, final List favoriteTools = const []}): _history = history,_connections = connections,_starred = starred,_favoriteTemplates = favoriteTemplates,_recentColors = recentColors,_flags = flags,_swamps = swamps,_favoriteTools = favoriteTools,super._(); + const _ButterflySettings({this.theme = ThemeMode.system, this.density = ThemeDensity.system, this.limitViewportMultiplier, this.limitViewportPositive = false, this.localeTag = '', this.documentPath = '', this.gestureSensitivity = 1, this.touchSensitivity = 1, this.selectSensitivity = 1, this.scrollSensitivity = 1, this.penOnlyInput, this.showPenOnlyToggle = true, this.inputGestures = true, this.design = '', this.bannerVisibility = BannerVisibility.always, @JsonKey(includeFromJson: false, includeToJson: false) final List history = const [], this.zoomEnabled = true, this.zoomPosition = ZoomPosition.bottomRight, this.propertyPosition = ZoomPosition.topRight, this.lastVersion, @JsonKey(includeFromJson: false, includeToJson: false) final List connections = const [], this.defaultRemote = '', this.nativeTitleBar = false, this.startInFullScreen = false, this.navigationRail = true, this.ignorePressure = IgnorePressure.first, this.syncMode = SyncMode.noMobile, this.inputConfiguration = const InputConfiguration(), this.fallbackPack = '', final List starred = const [], final List favoriteTemplates = const [], this.defaultTemplate = '', this.defaultFileName = kDefaultFileName, this.navigatorPosition = NavigatorPosition.left, this.toolbarPosition = ToolbarPosition.inline, this.toolbarSize = ToolbarSize.normal, this.sortBy = SortBy.modified, this.sortOrder = SortOrder.descending, this.imageScale = 0.5, this.platformTheme = PlatformTheme.system, @SRGBConverter() final List recentColors = const [], final List flags = const [], this.spreadPages = false, this.highContrast = false, this.gridView = false, this.hideExtension = true, this.autosave = true, this.showSaveButton = true, this.toolbarRows = 1, this.delayedAutosave = true, this.autosaveDelaySeconds = 3, this.hideCursorWhileDrawing = false, this.onStartup = StartupBehavior.openHomeScreen, this.documentStatePersistence = const DocumentStatePersistenceSettings(), this.simpleToolbarVisibility = SimpleToolbarVisibility.show, this.optionsPanelPosition = OptionsPanelPosition.top, this.renderResolution = RenderResolution.normal, this.moveOnGesture = true, final List swamps = const [], this.selectedPalette, this.showVerboseLogs = false, this.showThumbnails = true, this.bringMovedElementsToFront = false, final List favoriteTools = const []}): _history = history,_connections = connections,_starred = starred,_favoriteTemplates = favoriteTemplates,_recentColors = recentColors,_flags = flags,_swamps = swamps,_favoriteTools = favoriteTools,super._(); factory _ButterflySettings.fromJson(Map json) => _$ButterflySettingsFromJson(json); @override@JsonKey() final ThemeMode theme; @@ -932,6 +933,7 @@ class _ButterflySettings extends ButterflySettings with DiagnosticableTreeMixin } @override@JsonKey() final String defaultTemplate; +@override@JsonKey() final String defaultFileName; @override@JsonKey() final NavigatorPosition navigatorPosition; @override@JsonKey() final ToolbarPosition toolbarPosition; @override@JsonKey() final ToolbarSize toolbarSize; @@ -1002,21 +1004,21 @@ Map toJson() { void debugFillProperties(DiagnosticPropertiesBuilder properties) { properties ..add(DiagnosticsProperty('type', 'ButterflySettings')) - ..add(DiagnosticsProperty('theme', theme))..add(DiagnosticsProperty('density', density))..add(DiagnosticsProperty('limitViewportMultiplier', limitViewportMultiplier))..add(DiagnosticsProperty('limitViewportPositive', limitViewportPositive))..add(DiagnosticsProperty('localeTag', localeTag))..add(DiagnosticsProperty('documentPath', documentPath))..add(DiagnosticsProperty('gestureSensitivity', gestureSensitivity))..add(DiagnosticsProperty('touchSensitivity', touchSensitivity))..add(DiagnosticsProperty('selectSensitivity', selectSensitivity))..add(DiagnosticsProperty('scrollSensitivity', scrollSensitivity))..add(DiagnosticsProperty('penOnlyInput', penOnlyInput))..add(DiagnosticsProperty('showPenOnlyToggle', showPenOnlyToggle))..add(DiagnosticsProperty('inputGestures', inputGestures))..add(DiagnosticsProperty('design', design))..add(DiagnosticsProperty('bannerVisibility', bannerVisibility))..add(DiagnosticsProperty('history', history))..add(DiagnosticsProperty('zoomEnabled', zoomEnabled))..add(DiagnosticsProperty('zoomPosition', zoomPosition))..add(DiagnosticsProperty('propertyPosition', propertyPosition))..add(DiagnosticsProperty('lastVersion', lastVersion))..add(DiagnosticsProperty('connections', connections))..add(DiagnosticsProperty('defaultRemote', defaultRemote))..add(DiagnosticsProperty('nativeTitleBar', nativeTitleBar))..add(DiagnosticsProperty('startInFullScreen', startInFullScreen))..add(DiagnosticsProperty('navigationRail', navigationRail))..add(DiagnosticsProperty('ignorePressure', ignorePressure))..add(DiagnosticsProperty('syncMode', syncMode))..add(DiagnosticsProperty('inputConfiguration', inputConfiguration))..add(DiagnosticsProperty('fallbackPack', fallbackPack))..add(DiagnosticsProperty('starred', starred))..add(DiagnosticsProperty('favoriteTemplates', favoriteTemplates))..add(DiagnosticsProperty('defaultTemplate', defaultTemplate))..add(DiagnosticsProperty('navigatorPosition', navigatorPosition))..add(DiagnosticsProperty('toolbarPosition', toolbarPosition))..add(DiagnosticsProperty('toolbarSize', toolbarSize))..add(DiagnosticsProperty('sortBy', sortBy))..add(DiagnosticsProperty('sortOrder', sortOrder))..add(DiagnosticsProperty('imageScale', imageScale))..add(DiagnosticsProperty('platformTheme', platformTheme))..add(DiagnosticsProperty('recentColors', recentColors))..add(DiagnosticsProperty('flags', flags))..add(DiagnosticsProperty('spreadPages', spreadPages))..add(DiagnosticsProperty('highContrast', highContrast))..add(DiagnosticsProperty('gridView', gridView))..add(DiagnosticsProperty('hideExtension', hideExtension))..add(DiagnosticsProperty('autosave', autosave))..add(DiagnosticsProperty('showSaveButton', showSaveButton))..add(DiagnosticsProperty('toolbarRows', toolbarRows))..add(DiagnosticsProperty('delayedAutosave', delayedAutosave))..add(DiagnosticsProperty('autosaveDelaySeconds', autosaveDelaySeconds))..add(DiagnosticsProperty('hideCursorWhileDrawing', hideCursorWhileDrawing))..add(DiagnosticsProperty('onStartup', onStartup))..add(DiagnosticsProperty('documentStatePersistence', documentStatePersistence))..add(DiagnosticsProperty('simpleToolbarVisibility', simpleToolbarVisibility))..add(DiagnosticsProperty('optionsPanelPosition', optionsPanelPosition))..add(DiagnosticsProperty('renderResolution', renderResolution))..add(DiagnosticsProperty('moveOnGesture', moveOnGesture))..add(DiagnosticsProperty('swamps', swamps))..add(DiagnosticsProperty('selectedPalette', selectedPalette))..add(DiagnosticsProperty('showVerboseLogs', showVerboseLogs))..add(DiagnosticsProperty('showThumbnails', showThumbnails))..add(DiagnosticsProperty('bringMovedElementsToFront', bringMovedElementsToFront))..add(DiagnosticsProperty('favoriteTools', favoriteTools)); + ..add(DiagnosticsProperty('theme', theme))..add(DiagnosticsProperty('density', density))..add(DiagnosticsProperty('limitViewportMultiplier', limitViewportMultiplier))..add(DiagnosticsProperty('limitViewportPositive', limitViewportPositive))..add(DiagnosticsProperty('localeTag', localeTag))..add(DiagnosticsProperty('documentPath', documentPath))..add(DiagnosticsProperty('gestureSensitivity', gestureSensitivity))..add(DiagnosticsProperty('touchSensitivity', touchSensitivity))..add(DiagnosticsProperty('selectSensitivity', selectSensitivity))..add(DiagnosticsProperty('scrollSensitivity', scrollSensitivity))..add(DiagnosticsProperty('penOnlyInput', penOnlyInput))..add(DiagnosticsProperty('showPenOnlyToggle', showPenOnlyToggle))..add(DiagnosticsProperty('inputGestures', inputGestures))..add(DiagnosticsProperty('design', design))..add(DiagnosticsProperty('bannerVisibility', bannerVisibility))..add(DiagnosticsProperty('history', history))..add(DiagnosticsProperty('zoomEnabled', zoomEnabled))..add(DiagnosticsProperty('zoomPosition', zoomPosition))..add(DiagnosticsProperty('propertyPosition', propertyPosition))..add(DiagnosticsProperty('lastVersion', lastVersion))..add(DiagnosticsProperty('connections', connections))..add(DiagnosticsProperty('defaultRemote', defaultRemote))..add(DiagnosticsProperty('nativeTitleBar', nativeTitleBar))..add(DiagnosticsProperty('startInFullScreen', startInFullScreen))..add(DiagnosticsProperty('navigationRail', navigationRail))..add(DiagnosticsProperty('ignorePressure', ignorePressure))..add(DiagnosticsProperty('syncMode', syncMode))..add(DiagnosticsProperty('inputConfiguration', inputConfiguration))..add(DiagnosticsProperty('fallbackPack', fallbackPack))..add(DiagnosticsProperty('starred', starred))..add(DiagnosticsProperty('favoriteTemplates', favoriteTemplates))..add(DiagnosticsProperty('defaultTemplate', defaultTemplate))..add(DiagnosticsProperty('defaultFileName', defaultFileName))..add(DiagnosticsProperty('navigatorPosition', navigatorPosition))..add(DiagnosticsProperty('toolbarPosition', toolbarPosition))..add(DiagnosticsProperty('toolbarSize', toolbarSize))..add(DiagnosticsProperty('sortBy', sortBy))..add(DiagnosticsProperty('sortOrder', sortOrder))..add(DiagnosticsProperty('imageScale', imageScale))..add(DiagnosticsProperty('platformTheme', platformTheme))..add(DiagnosticsProperty('recentColors', recentColors))..add(DiagnosticsProperty('flags', flags))..add(DiagnosticsProperty('spreadPages', spreadPages))..add(DiagnosticsProperty('highContrast', highContrast))..add(DiagnosticsProperty('gridView', gridView))..add(DiagnosticsProperty('hideExtension', hideExtension))..add(DiagnosticsProperty('autosave', autosave))..add(DiagnosticsProperty('showSaveButton', showSaveButton))..add(DiagnosticsProperty('toolbarRows', toolbarRows))..add(DiagnosticsProperty('delayedAutosave', delayedAutosave))..add(DiagnosticsProperty('autosaveDelaySeconds', autosaveDelaySeconds))..add(DiagnosticsProperty('hideCursorWhileDrawing', hideCursorWhileDrawing))..add(DiagnosticsProperty('onStartup', onStartup))..add(DiagnosticsProperty('documentStatePersistence', documentStatePersistence))..add(DiagnosticsProperty('simpleToolbarVisibility', simpleToolbarVisibility))..add(DiagnosticsProperty('optionsPanelPosition', optionsPanelPosition))..add(DiagnosticsProperty('renderResolution', renderResolution))..add(DiagnosticsProperty('moveOnGesture', moveOnGesture))..add(DiagnosticsProperty('swamps', swamps))..add(DiagnosticsProperty('selectedPalette', selectedPalette))..add(DiagnosticsProperty('showVerboseLogs', showVerboseLogs))..add(DiagnosticsProperty('showThumbnails', showThumbnails))..add(DiagnosticsProperty('bringMovedElementsToFront', bringMovedElementsToFront))..add(DiagnosticsProperty('favoriteTools', favoriteTools)); } @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _ButterflySettings&&(identical(other.theme, theme) || other.theme == theme)&&(identical(other.density, density) || other.density == density)&&(identical(other.limitViewportMultiplier, limitViewportMultiplier) || other.limitViewportMultiplier == limitViewportMultiplier)&&(identical(other.limitViewportPositive, limitViewportPositive) || other.limitViewportPositive == limitViewportPositive)&&(identical(other.localeTag, localeTag) || other.localeTag == localeTag)&&(identical(other.documentPath, documentPath) || other.documentPath == documentPath)&&(identical(other.gestureSensitivity, gestureSensitivity) || other.gestureSensitivity == gestureSensitivity)&&(identical(other.touchSensitivity, touchSensitivity) || other.touchSensitivity == touchSensitivity)&&(identical(other.selectSensitivity, selectSensitivity) || other.selectSensitivity == selectSensitivity)&&(identical(other.scrollSensitivity, scrollSensitivity) || other.scrollSensitivity == scrollSensitivity)&&(identical(other.penOnlyInput, penOnlyInput) || other.penOnlyInput == penOnlyInput)&&(identical(other.showPenOnlyToggle, showPenOnlyToggle) || other.showPenOnlyToggle == showPenOnlyToggle)&&(identical(other.inputGestures, inputGestures) || other.inputGestures == inputGestures)&&(identical(other.design, design) || other.design == design)&&(identical(other.bannerVisibility, bannerVisibility) || other.bannerVisibility == bannerVisibility)&&const DeepCollectionEquality().equals(other._history, _history)&&(identical(other.zoomEnabled, zoomEnabled) || other.zoomEnabled == zoomEnabled)&&(identical(other.zoomPosition, zoomPosition) || other.zoomPosition == zoomPosition)&&(identical(other.propertyPosition, propertyPosition) || other.propertyPosition == propertyPosition)&&(identical(other.lastVersion, lastVersion) || other.lastVersion == lastVersion)&&const DeepCollectionEquality().equals(other._connections, _connections)&&(identical(other.defaultRemote, defaultRemote) || other.defaultRemote == defaultRemote)&&(identical(other.nativeTitleBar, nativeTitleBar) || other.nativeTitleBar == nativeTitleBar)&&(identical(other.startInFullScreen, startInFullScreen) || other.startInFullScreen == startInFullScreen)&&(identical(other.navigationRail, navigationRail) || other.navigationRail == navigationRail)&&(identical(other.ignorePressure, ignorePressure) || other.ignorePressure == ignorePressure)&&(identical(other.syncMode, syncMode) || other.syncMode == syncMode)&&(identical(other.inputConfiguration, inputConfiguration) || other.inputConfiguration == inputConfiguration)&&(identical(other.fallbackPack, fallbackPack) || other.fallbackPack == fallbackPack)&&const DeepCollectionEquality().equals(other._starred, _starred)&&const DeepCollectionEquality().equals(other._favoriteTemplates, _favoriteTemplates)&&(identical(other.defaultTemplate, defaultTemplate) || other.defaultTemplate == defaultTemplate)&&(identical(other.navigatorPosition, navigatorPosition) || other.navigatorPosition == navigatorPosition)&&(identical(other.toolbarPosition, toolbarPosition) || other.toolbarPosition == toolbarPosition)&&(identical(other.toolbarSize, toolbarSize) || other.toolbarSize == toolbarSize)&&(identical(other.sortBy, sortBy) || other.sortBy == sortBy)&&(identical(other.sortOrder, sortOrder) || other.sortOrder == sortOrder)&&(identical(other.imageScale, imageScale) || other.imageScale == imageScale)&&(identical(other.platformTheme, platformTheme) || other.platformTheme == platformTheme)&&const DeepCollectionEquality().equals(other._recentColors, _recentColors)&&const DeepCollectionEquality().equals(other._flags, _flags)&&(identical(other.spreadPages, spreadPages) || other.spreadPages == spreadPages)&&(identical(other.highContrast, highContrast) || other.highContrast == highContrast)&&(identical(other.gridView, gridView) || other.gridView == gridView)&&(identical(other.hideExtension, hideExtension) || other.hideExtension == hideExtension)&&(identical(other.autosave, autosave) || other.autosave == autosave)&&(identical(other.showSaveButton, showSaveButton) || other.showSaveButton == showSaveButton)&&(identical(other.toolbarRows, toolbarRows) || other.toolbarRows == toolbarRows)&&(identical(other.delayedAutosave, delayedAutosave) || other.delayedAutosave == delayedAutosave)&&(identical(other.autosaveDelaySeconds, autosaveDelaySeconds) || other.autosaveDelaySeconds == autosaveDelaySeconds)&&(identical(other.hideCursorWhileDrawing, hideCursorWhileDrawing) || other.hideCursorWhileDrawing == hideCursorWhileDrawing)&&(identical(other.onStartup, onStartup) || other.onStartup == onStartup)&&(identical(other.documentStatePersistence, documentStatePersistence) || other.documentStatePersistence == documentStatePersistence)&&(identical(other.simpleToolbarVisibility, simpleToolbarVisibility) || other.simpleToolbarVisibility == simpleToolbarVisibility)&&(identical(other.optionsPanelPosition, optionsPanelPosition) || other.optionsPanelPosition == optionsPanelPosition)&&(identical(other.renderResolution, renderResolution) || other.renderResolution == renderResolution)&&(identical(other.moveOnGesture, moveOnGesture) || other.moveOnGesture == moveOnGesture)&&const DeepCollectionEquality().equals(other._swamps, _swamps)&&(identical(other.selectedPalette, selectedPalette) || other.selectedPalette == selectedPalette)&&(identical(other.showVerboseLogs, showVerboseLogs) || other.showVerboseLogs == showVerboseLogs)&&(identical(other.showThumbnails, showThumbnails) || other.showThumbnails == showThumbnails)&&(identical(other.bringMovedElementsToFront, bringMovedElementsToFront) || other.bringMovedElementsToFront == bringMovedElementsToFront)&&const DeepCollectionEquality().equals(other._favoriteTools, _favoriteTools)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ButterflySettings&&(identical(other.theme, theme) || other.theme == theme)&&(identical(other.density, density) || other.density == density)&&(identical(other.limitViewportMultiplier, limitViewportMultiplier) || other.limitViewportMultiplier == limitViewportMultiplier)&&(identical(other.limitViewportPositive, limitViewportPositive) || other.limitViewportPositive == limitViewportPositive)&&(identical(other.localeTag, localeTag) || other.localeTag == localeTag)&&(identical(other.documentPath, documentPath) || other.documentPath == documentPath)&&(identical(other.gestureSensitivity, gestureSensitivity) || other.gestureSensitivity == gestureSensitivity)&&(identical(other.touchSensitivity, touchSensitivity) || other.touchSensitivity == touchSensitivity)&&(identical(other.selectSensitivity, selectSensitivity) || other.selectSensitivity == selectSensitivity)&&(identical(other.scrollSensitivity, scrollSensitivity) || other.scrollSensitivity == scrollSensitivity)&&(identical(other.penOnlyInput, penOnlyInput) || other.penOnlyInput == penOnlyInput)&&(identical(other.showPenOnlyToggle, showPenOnlyToggle) || other.showPenOnlyToggle == showPenOnlyToggle)&&(identical(other.inputGestures, inputGestures) || other.inputGestures == inputGestures)&&(identical(other.design, design) || other.design == design)&&(identical(other.bannerVisibility, bannerVisibility) || other.bannerVisibility == bannerVisibility)&&const DeepCollectionEquality().equals(other._history, _history)&&(identical(other.zoomEnabled, zoomEnabled) || other.zoomEnabled == zoomEnabled)&&(identical(other.zoomPosition, zoomPosition) || other.zoomPosition == zoomPosition)&&(identical(other.propertyPosition, propertyPosition) || other.propertyPosition == propertyPosition)&&(identical(other.lastVersion, lastVersion) || other.lastVersion == lastVersion)&&const DeepCollectionEquality().equals(other._connections, _connections)&&(identical(other.defaultRemote, defaultRemote) || other.defaultRemote == defaultRemote)&&(identical(other.nativeTitleBar, nativeTitleBar) || other.nativeTitleBar == nativeTitleBar)&&(identical(other.startInFullScreen, startInFullScreen) || other.startInFullScreen == startInFullScreen)&&(identical(other.navigationRail, navigationRail) || other.navigationRail == navigationRail)&&(identical(other.ignorePressure, ignorePressure) || other.ignorePressure == ignorePressure)&&(identical(other.syncMode, syncMode) || other.syncMode == syncMode)&&(identical(other.inputConfiguration, inputConfiguration) || other.inputConfiguration == inputConfiguration)&&(identical(other.fallbackPack, fallbackPack) || other.fallbackPack == fallbackPack)&&const DeepCollectionEquality().equals(other._starred, _starred)&&const DeepCollectionEquality().equals(other._favoriteTemplates, _favoriteTemplates)&&(identical(other.defaultTemplate, defaultTemplate) || other.defaultTemplate == defaultTemplate)&&(identical(other.defaultFileName, defaultFileName) || other.defaultFileName == defaultFileName)&&(identical(other.navigatorPosition, navigatorPosition) || other.navigatorPosition == navigatorPosition)&&(identical(other.toolbarPosition, toolbarPosition) || other.toolbarPosition == toolbarPosition)&&(identical(other.toolbarSize, toolbarSize) || other.toolbarSize == toolbarSize)&&(identical(other.sortBy, sortBy) || other.sortBy == sortBy)&&(identical(other.sortOrder, sortOrder) || other.sortOrder == sortOrder)&&(identical(other.imageScale, imageScale) || other.imageScale == imageScale)&&(identical(other.platformTheme, platformTheme) || other.platformTheme == platformTheme)&&const DeepCollectionEquality().equals(other._recentColors, _recentColors)&&const DeepCollectionEquality().equals(other._flags, _flags)&&(identical(other.spreadPages, spreadPages) || other.spreadPages == spreadPages)&&(identical(other.highContrast, highContrast) || other.highContrast == highContrast)&&(identical(other.gridView, gridView) || other.gridView == gridView)&&(identical(other.hideExtension, hideExtension) || other.hideExtension == hideExtension)&&(identical(other.autosave, autosave) || other.autosave == autosave)&&(identical(other.showSaveButton, showSaveButton) || other.showSaveButton == showSaveButton)&&(identical(other.toolbarRows, toolbarRows) || other.toolbarRows == toolbarRows)&&(identical(other.delayedAutosave, delayedAutosave) || other.delayedAutosave == delayedAutosave)&&(identical(other.autosaveDelaySeconds, autosaveDelaySeconds) || other.autosaveDelaySeconds == autosaveDelaySeconds)&&(identical(other.hideCursorWhileDrawing, hideCursorWhileDrawing) || other.hideCursorWhileDrawing == hideCursorWhileDrawing)&&(identical(other.onStartup, onStartup) || other.onStartup == onStartup)&&(identical(other.documentStatePersistence, documentStatePersistence) || other.documentStatePersistence == documentStatePersistence)&&(identical(other.simpleToolbarVisibility, simpleToolbarVisibility) || other.simpleToolbarVisibility == simpleToolbarVisibility)&&(identical(other.optionsPanelPosition, optionsPanelPosition) || other.optionsPanelPosition == optionsPanelPosition)&&(identical(other.renderResolution, renderResolution) || other.renderResolution == renderResolution)&&(identical(other.moveOnGesture, moveOnGesture) || other.moveOnGesture == moveOnGesture)&&const DeepCollectionEquality().equals(other._swamps, _swamps)&&(identical(other.selectedPalette, selectedPalette) || other.selectedPalette == selectedPalette)&&(identical(other.showVerboseLogs, showVerboseLogs) || other.showVerboseLogs == showVerboseLogs)&&(identical(other.showThumbnails, showThumbnails) || other.showThumbnails == showThumbnails)&&(identical(other.bringMovedElementsToFront, bringMovedElementsToFront) || other.bringMovedElementsToFront == bringMovedElementsToFront)&&const DeepCollectionEquality().equals(other._favoriteTools, _favoriteTools)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hashAll([runtimeType,theme,density,limitViewportMultiplier,limitViewportPositive,localeTag,documentPath,gestureSensitivity,touchSensitivity,selectSensitivity,scrollSensitivity,penOnlyInput,showPenOnlyToggle,inputGestures,design,bannerVisibility,const DeepCollectionEquality().hash(_history),zoomEnabled,zoomPosition,propertyPosition,lastVersion,const DeepCollectionEquality().hash(_connections),defaultRemote,nativeTitleBar,startInFullScreen,navigationRail,ignorePressure,syncMode,inputConfiguration,fallbackPack,const DeepCollectionEquality().hash(_starred),const DeepCollectionEquality().hash(_favoriteTemplates),defaultTemplate,navigatorPosition,toolbarPosition,toolbarSize,sortBy,sortOrder,imageScale,platformTheme,const DeepCollectionEquality().hash(_recentColors),const DeepCollectionEquality().hash(_flags),spreadPages,highContrast,gridView,hideExtension,autosave,showSaveButton,toolbarRows,delayedAutosave,autosaveDelaySeconds,hideCursorWhileDrawing,onStartup,documentStatePersistence,simpleToolbarVisibility,optionsPanelPosition,renderResolution,moveOnGesture,const DeepCollectionEquality().hash(_swamps),selectedPalette,showVerboseLogs,showThumbnails,bringMovedElementsToFront,const DeepCollectionEquality().hash(_favoriteTools)]); +int get hashCode => Object.hashAll([runtimeType,theme,density,limitViewportMultiplier,limitViewportPositive,localeTag,documentPath,gestureSensitivity,touchSensitivity,selectSensitivity,scrollSensitivity,penOnlyInput,showPenOnlyToggle,inputGestures,design,bannerVisibility,const DeepCollectionEquality().hash(_history),zoomEnabled,zoomPosition,propertyPosition,lastVersion,const DeepCollectionEquality().hash(_connections),defaultRemote,nativeTitleBar,startInFullScreen,navigationRail,ignorePressure,syncMode,inputConfiguration,fallbackPack,const DeepCollectionEquality().hash(_starred),const DeepCollectionEquality().hash(_favoriteTemplates),defaultTemplate,defaultFileName,navigatorPosition,toolbarPosition,toolbarSize,sortBy,sortOrder,imageScale,platformTheme,const DeepCollectionEquality().hash(_recentColors),const DeepCollectionEquality().hash(_flags),spreadPages,highContrast,gridView,hideExtension,autosave,showSaveButton,toolbarRows,delayedAutosave,autosaveDelaySeconds,hideCursorWhileDrawing,onStartup,documentStatePersistence,simpleToolbarVisibility,optionsPanelPosition,renderResolution,moveOnGesture,const DeepCollectionEquality().hash(_swamps),selectedPalette,showVerboseLogs,showThumbnails,bringMovedElementsToFront,const DeepCollectionEquality().hash(_favoriteTools)]); @override String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'ButterflySettings(theme: $theme, density: $density, limitViewportMultiplier: $limitViewportMultiplier, limitViewportPositive: $limitViewportPositive, localeTag: $localeTag, documentPath: $documentPath, gestureSensitivity: $gestureSensitivity, touchSensitivity: $touchSensitivity, selectSensitivity: $selectSensitivity, scrollSensitivity: $scrollSensitivity, penOnlyInput: $penOnlyInput, showPenOnlyToggle: $showPenOnlyToggle, inputGestures: $inputGestures, design: $design, bannerVisibility: $bannerVisibility, history: $history, zoomEnabled: $zoomEnabled, zoomPosition: $zoomPosition, propertyPosition: $propertyPosition, lastVersion: $lastVersion, connections: $connections, defaultRemote: $defaultRemote, nativeTitleBar: $nativeTitleBar, startInFullScreen: $startInFullScreen, navigationRail: $navigationRail, ignorePressure: $ignorePressure, syncMode: $syncMode, inputConfiguration: $inputConfiguration, fallbackPack: $fallbackPack, starred: $starred, favoriteTemplates: $favoriteTemplates, defaultTemplate: $defaultTemplate, navigatorPosition: $navigatorPosition, toolbarPosition: $toolbarPosition, toolbarSize: $toolbarSize, sortBy: $sortBy, sortOrder: $sortOrder, imageScale: $imageScale, platformTheme: $platformTheme, recentColors: $recentColors, flags: $flags, spreadPages: $spreadPages, highContrast: $highContrast, gridView: $gridView, hideExtension: $hideExtension, autosave: $autosave, showSaveButton: $showSaveButton, toolbarRows: $toolbarRows, delayedAutosave: $delayedAutosave, autosaveDelaySeconds: $autosaveDelaySeconds, hideCursorWhileDrawing: $hideCursorWhileDrawing, onStartup: $onStartup, documentStatePersistence: $documentStatePersistence, simpleToolbarVisibility: $simpleToolbarVisibility, optionsPanelPosition: $optionsPanelPosition, renderResolution: $renderResolution, moveOnGesture: $moveOnGesture, swamps: $swamps, selectedPalette: $selectedPalette, showVerboseLogs: $showVerboseLogs, showThumbnails: $showThumbnails, bringMovedElementsToFront: $bringMovedElementsToFront, favoriteTools: $favoriteTools)'; + return 'ButterflySettings(theme: $theme, density: $density, limitViewportMultiplier: $limitViewportMultiplier, limitViewportPositive: $limitViewportPositive, localeTag: $localeTag, documentPath: $documentPath, gestureSensitivity: $gestureSensitivity, touchSensitivity: $touchSensitivity, selectSensitivity: $selectSensitivity, scrollSensitivity: $scrollSensitivity, penOnlyInput: $penOnlyInput, showPenOnlyToggle: $showPenOnlyToggle, inputGestures: $inputGestures, design: $design, bannerVisibility: $bannerVisibility, history: $history, zoomEnabled: $zoomEnabled, zoomPosition: $zoomPosition, propertyPosition: $propertyPosition, lastVersion: $lastVersion, connections: $connections, defaultRemote: $defaultRemote, nativeTitleBar: $nativeTitleBar, startInFullScreen: $startInFullScreen, navigationRail: $navigationRail, ignorePressure: $ignorePressure, syncMode: $syncMode, inputConfiguration: $inputConfiguration, fallbackPack: $fallbackPack, starred: $starred, favoriteTemplates: $favoriteTemplates, defaultTemplate: $defaultTemplate, defaultFileName: $defaultFileName, navigatorPosition: $navigatorPosition, toolbarPosition: $toolbarPosition, toolbarSize: $toolbarSize, sortBy: $sortBy, sortOrder: $sortOrder, imageScale: $imageScale, platformTheme: $platformTheme, recentColors: $recentColors, flags: $flags, spreadPages: $spreadPages, highContrast: $highContrast, gridView: $gridView, hideExtension: $hideExtension, autosave: $autosave, showSaveButton: $showSaveButton, toolbarRows: $toolbarRows, delayedAutosave: $delayedAutosave, autosaveDelaySeconds: $autosaveDelaySeconds, hideCursorWhileDrawing: $hideCursorWhileDrawing, onStartup: $onStartup, documentStatePersistence: $documentStatePersistence, simpleToolbarVisibility: $simpleToolbarVisibility, optionsPanelPosition: $optionsPanelPosition, renderResolution: $renderResolution, moveOnGesture: $moveOnGesture, swamps: $swamps, selectedPalette: $selectedPalette, showVerboseLogs: $showVerboseLogs, showThumbnails: $showThumbnails, bringMovedElementsToFront: $bringMovedElementsToFront, favoriteTools: $favoriteTools)'; } @@ -1027,7 +1029,7 @@ abstract mixin class _$ButterflySettingsCopyWith<$Res> implements $ButterflySett factory _$ButterflySettingsCopyWith(_ButterflySettings value, $Res Function(_ButterflySettings) _then) = __$ButterflySettingsCopyWithImpl; @override @useResult $Res call({ - ThemeMode theme, ThemeDensity density, double? limitViewportMultiplier, bool limitViewportPositive, String localeTag, String documentPath, double gestureSensitivity, double touchSensitivity, double selectSensitivity, double scrollSensitivity, bool? penOnlyInput, bool showPenOnlyToggle, bool inputGestures, String design, BannerVisibility bannerVisibility,@JsonKey(includeFromJson: false, includeToJson: false) List history, bool zoomEnabled, ZoomPosition zoomPosition, ZoomPosition propertyPosition, String? lastVersion,@JsonKey(includeFromJson: false, includeToJson: false) List connections, String defaultRemote, bool nativeTitleBar, bool startInFullScreen, bool navigationRail, IgnorePressure ignorePressure, SyncMode syncMode, InputConfiguration inputConfiguration, String fallbackPack, List starred, List favoriteTemplates, String defaultTemplate, NavigatorPosition navigatorPosition, ToolbarPosition toolbarPosition, ToolbarSize toolbarSize, SortBy sortBy, SortOrder sortOrder, double imageScale, PlatformTheme platformTheme,@SRGBConverter() List recentColors, List flags, bool spreadPages, bool highContrast, bool gridView, bool hideExtension, bool autosave, bool showSaveButton, int toolbarRows, bool delayedAutosave, int autosaveDelaySeconds, bool hideCursorWhileDrawing, StartupBehavior onStartup, DocumentStatePersistenceSettings documentStatePersistence, SimpleToolbarVisibility simpleToolbarVisibility, OptionsPanelPosition optionsPanelPosition, RenderResolution renderResolution, bool moveOnGesture, List swamps, PackAssetLocation? selectedPalette, bool showVerboseLogs, bool showThumbnails, bool bringMovedElementsToFront, List favoriteTools + ThemeMode theme, ThemeDensity density, double? limitViewportMultiplier, bool limitViewportPositive, String localeTag, String documentPath, double gestureSensitivity, double touchSensitivity, double selectSensitivity, double scrollSensitivity, bool? penOnlyInput, bool showPenOnlyToggle, bool inputGestures, String design, BannerVisibility bannerVisibility,@JsonKey(includeFromJson: false, includeToJson: false) List history, bool zoomEnabled, ZoomPosition zoomPosition, ZoomPosition propertyPosition, String? lastVersion,@JsonKey(includeFromJson: false, includeToJson: false) List connections, String defaultRemote, bool nativeTitleBar, bool startInFullScreen, bool navigationRail, IgnorePressure ignorePressure, SyncMode syncMode, InputConfiguration inputConfiguration, String fallbackPack, List starred, List favoriteTemplates, String defaultTemplate, String defaultFileName, NavigatorPosition navigatorPosition, ToolbarPosition toolbarPosition, ToolbarSize toolbarSize, SortBy sortBy, SortOrder sortOrder, double imageScale, PlatformTheme platformTheme,@SRGBConverter() List recentColors, List flags, bool spreadPages, bool highContrast, bool gridView, bool hideExtension, bool autosave, bool showSaveButton, int toolbarRows, bool delayedAutosave, int autosaveDelaySeconds, bool hideCursorWhileDrawing, StartupBehavior onStartup, DocumentStatePersistenceSettings documentStatePersistence, SimpleToolbarVisibility simpleToolbarVisibility, OptionsPanelPosition optionsPanelPosition, RenderResolution renderResolution, bool moveOnGesture, List swamps, PackAssetLocation? selectedPalette, bool showVerboseLogs, bool showThumbnails, bool bringMovedElementsToFront, List favoriteTools }); @@ -1044,7 +1046,7 @@ class __$ButterflySettingsCopyWithImpl<$Res> /// Create a copy of ButterflySettings /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? theme = null,Object? density = null,Object? limitViewportMultiplier = freezed,Object? limitViewportPositive = null,Object? localeTag = null,Object? documentPath = null,Object? gestureSensitivity = null,Object? touchSensitivity = null,Object? selectSensitivity = null,Object? scrollSensitivity = null,Object? penOnlyInput = freezed,Object? showPenOnlyToggle = null,Object? inputGestures = null,Object? design = null,Object? bannerVisibility = null,Object? history = null,Object? zoomEnabled = null,Object? zoomPosition = null,Object? propertyPosition = null,Object? lastVersion = freezed,Object? connections = null,Object? defaultRemote = null,Object? nativeTitleBar = null,Object? startInFullScreen = null,Object? navigationRail = null,Object? ignorePressure = null,Object? syncMode = null,Object? inputConfiguration = null,Object? fallbackPack = null,Object? starred = null,Object? favoriteTemplates = null,Object? defaultTemplate = null,Object? navigatorPosition = null,Object? toolbarPosition = null,Object? toolbarSize = null,Object? sortBy = null,Object? sortOrder = null,Object? imageScale = null,Object? platformTheme = null,Object? recentColors = null,Object? flags = null,Object? spreadPages = null,Object? highContrast = null,Object? gridView = null,Object? hideExtension = null,Object? autosave = null,Object? showSaveButton = null,Object? toolbarRows = null,Object? delayedAutosave = null,Object? autosaveDelaySeconds = null,Object? hideCursorWhileDrawing = null,Object? onStartup = null,Object? documentStatePersistence = null,Object? simpleToolbarVisibility = null,Object? optionsPanelPosition = null,Object? renderResolution = null,Object? moveOnGesture = null,Object? swamps = null,Object? selectedPalette = freezed,Object? showVerboseLogs = null,Object? showThumbnails = null,Object? bringMovedElementsToFront = null,Object? favoriteTools = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? theme = null,Object? density = null,Object? limitViewportMultiplier = freezed,Object? limitViewportPositive = null,Object? localeTag = null,Object? documentPath = null,Object? gestureSensitivity = null,Object? touchSensitivity = null,Object? selectSensitivity = null,Object? scrollSensitivity = null,Object? penOnlyInput = freezed,Object? showPenOnlyToggle = null,Object? inputGestures = null,Object? design = null,Object? bannerVisibility = null,Object? history = null,Object? zoomEnabled = null,Object? zoomPosition = null,Object? propertyPosition = null,Object? lastVersion = freezed,Object? connections = null,Object? defaultRemote = null,Object? nativeTitleBar = null,Object? startInFullScreen = null,Object? navigationRail = null,Object? ignorePressure = null,Object? syncMode = null,Object? inputConfiguration = null,Object? fallbackPack = null,Object? starred = null,Object? favoriteTemplates = null,Object? defaultTemplate = null,Object? defaultFileName = null,Object? navigatorPosition = null,Object? toolbarPosition = null,Object? toolbarSize = null,Object? sortBy = null,Object? sortOrder = null,Object? imageScale = null,Object? platformTheme = null,Object? recentColors = null,Object? flags = null,Object? spreadPages = null,Object? highContrast = null,Object? gridView = null,Object? hideExtension = null,Object? autosave = null,Object? showSaveButton = null,Object? toolbarRows = null,Object? delayedAutosave = null,Object? autosaveDelaySeconds = null,Object? hideCursorWhileDrawing = null,Object? onStartup = null,Object? documentStatePersistence = null,Object? simpleToolbarVisibility = null,Object? optionsPanelPosition = null,Object? renderResolution = null,Object? moveOnGesture = null,Object? swamps = null,Object? selectedPalette = freezed,Object? showVerboseLogs = null,Object? showThumbnails = null,Object? bringMovedElementsToFront = null,Object? favoriteTools = null,}) { return _then(_ButterflySettings( theme: null == theme ? _self.theme : theme // ignore: cast_nullable_to_non_nullable as ThemeMode,density: null == density ? _self.density : density // ignore: cast_nullable_to_non_nullable @@ -1078,6 +1080,7 @@ as InputConfiguration,fallbackPack: null == fallbackPack ? _self.fallbackPack : as String,starred: null == starred ? _self._starred : starred // ignore: cast_nullable_to_non_nullable as List,favoriteTemplates: null == favoriteTemplates ? _self._favoriteTemplates : favoriteTemplates // ignore: cast_nullable_to_non_nullable as List,defaultTemplate: null == defaultTemplate ? _self.defaultTemplate : defaultTemplate // ignore: cast_nullable_to_non_nullable +as String,defaultFileName: null == defaultFileName ? _self.defaultFileName : defaultFileName // ignore: cast_nullable_to_non_nullable as String,navigatorPosition: null == navigatorPosition ? _self.navigatorPosition : navigatorPosition // ignore: cast_nullable_to_non_nullable as NavigatorPosition,toolbarPosition: null == toolbarPosition ? _self.toolbarPosition : toolbarPosition // ignore: cast_nullable_to_non_nullable as ToolbarPosition,toolbarSize: null == toolbarSize ? _self.toolbarSize : toolbarSize // ignore: cast_nullable_to_non_nullable diff --git a/app/lib/cubits/settings.g.dart b/app/lib/cubits/settings.g.dart index aee81700007d..5e3faba7a53a 100644 --- a/app/lib/cubits/settings.g.dart +++ b/app/lib/cubits/settings.g.dart @@ -200,6 +200,7 @@ _ButterflySettings _$ButterflySettingsFromJson(Map json) => _ButterflySettings( .toList() ?? const [], defaultTemplate: json['defaultTemplate'] as String? ?? '', + defaultFileName: json['defaultFileName'] as String? ?? kDefaultFileName, navigatorPosition: $enumDecodeNullable( _$NavigatorPositionEnumMap, @@ -323,6 +324,7 @@ Map _$ButterflySettingsToJson( .map((e) => e.toJson()) .toList(), 'defaultTemplate': instance.defaultTemplate, + 'defaultFileName': instance.defaultFileName, 'navigatorPosition': _$NavigatorPositionEnumMap[instance.navigatorPosition]!, 'toolbarPosition': _$ToolbarPositionEnumMap[instance.toolbarPosition]!, 'toolbarSize': _$ToolbarSizeEnumMap[instance.toolbarSize]!, diff --git a/app/lib/dialogs/template.dart b/app/lib/dialogs/template.dart index b3563e3c8a3a..f7559c9c9fd5 100644 --- a/app/lib/dialogs/template.dart +++ b/app/lib/dialogs/template.dart @@ -8,6 +8,7 @@ import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/models/defaults.dart'; import 'package:butterfly/visualizer/tool.dart'; import 'package:butterfly/widgets/connection_button.dart'; +import 'package:butterfly/widgets/file_name_pattern_field.dart'; import 'package:butterfly/widgets/option_button.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:collection/collection.dart'; @@ -24,69 +25,6 @@ import 'area/init.dart'; import 'delete.dart'; import 'pages.dart'; -String? _validateOptionalTemplateFileName(BuildContext context, String? value) { - final fileName = value?.trim() ?? ''; - if (fileName.isEmpty) return null; - try { - final resolved = resolveTemplateFileName( - fileName, - DateTime(2000, 12, 31, 23, 59, 58), - ); - return defaultFileNameValidator(context)(null)(resolved); - } on FormatException { - return LeapLocalizations.of(context).invalidName; - } -} - -String _templateFileNameDescription(BuildContext context) => - AppLocalizations.of(context).templateFileNameDescription( - templateDateFormatExample, - templateTimeFormatExample, - ); - -Future _showTemplateFileNameDialog( - BuildContext context, - String initialValue, -) { - final formKey = GlobalKey(); - var fileName = initialValue; - return showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(AppLocalizations.of(context).fileName), - content: Form( - key: formKey, - child: TextFormField( - initialValue: fileName, - autofocus: true, - onChanged: (value) => fileName = value, - validator: (value) => - _validateOptionalTemplateFileName(context, value), - decoration: InputDecoration( - labelText: AppLocalizations.of(context).fileName, - helperText: _templateFileNameDescription(context), - helperMaxLines: 3, - filled: true, - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), - ElevatedButton( - onPressed: () { - if (!(formKey.currentState?.validate() ?? false)) return; - Navigator.of(context).pop(fileName.trim()); - }, - child: Text(MaterialLocalizations.of(context).saveButtonLabel), - ), - ], - ), - ); -} - Future _overrideTools( TemplateFileSystem templateSystem, DocumentBloc bloc, @@ -762,17 +700,10 @@ class _TemplateDialogState extends State { ), ), const SizedBox(height: 8), - TextFormField( + FileNamePatternField( initialValue: fileName, + label: AppLocalizations.of(context).fileName, onChanged: (e) => fileName = e, - validator: (value) => - _validateOptionalTemplateFileName(context, value), - decoration: InputDecoration( - labelText: AppLocalizations.of(context).fileName, - helperText: _templateFileNameDescription(context), - helperMaxLines: 3, - filled: true, - ), ), const SizedBox(height: 8), TextFormField( @@ -1472,9 +1403,10 @@ List _buildTemplateMenuChildren( leadingIcon: const PhosphorIcon(PhosphorIconsLight.fileText), child: Text(AppLocalizations.of(context).fileName), onPressed: () async { - final result = await _showTemplateFileNameDialog( + final result = await showFileNamePatternDialog( context, - metadata.fileName, + initialValue: metadata.fileName, + label: AppLocalizations.of(context).fileName, ); if (result == null) return; await fileSystem.updateFile( diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 37083b3e23bb..cb1952d3a78c 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -351,19 +351,21 @@ "createTemplate": "Create template", "createTemplateContent": "Do you really want to create a template from this document? The original document will get deleted.", "fileName": "File name", - "templateFileNameDescription": "Set a name to save new documents immediately. Use a custom date or time formatter, for example {dateExample} or {timeExample}.", + "defaultFileName": "Default file name", + "templateFileNameDescription": "New documents are saved automatically with this name. Combine your own text with {dateExample} and {timeExample}. Characters that are not allowed in file names will be rejected.", "@templateFileNameDescription": { "placeholders": { "dateExample": { "type": "String", - "example": "{date:dd.MM.yyyy}" + "example": "{date}" }, "timeExample": { "type": "String", - "example": "{time:HH-mm}" + "example": "{time}" } } }, + "preview": "Preview", "replace": "Replace", "@replace": { "description": "Replace action" diff --git a/app/lib/settings/data.dart b/app/lib/settings/data.dart index 13fc919cb8aa..915f7a2e5e13 100644 --- a/app/lib/settings/data.dart +++ b/app/lib/settings/data.dart @@ -11,6 +11,7 @@ import 'package:butterfly/cubits/transform.dart'; import 'package:butterfly/dialogs/template.dart'; import 'package:butterfly/models/viewport.dart'; import 'package:butterfly/visualizer/connection.dart'; +import 'package:butterfly/widgets/file_name_pattern_field.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -67,6 +68,41 @@ Widget buildDataDirectorySetting( ); } +Widget buildDefaultFileNameSetting( + BuildContext context, + ButterflySettings state, +) { + return ListTile( + leading: const PhosphorIcon(PhosphorIconsLight.fileText), + title: Text(AppLocalizations.of(context).defaultFileName), + subtitle: Text(state.defaultFileName), + onTap: () => _changeDefaultFileName(context, state.defaultFileName), + trailing: state.defaultFileName == kDefaultFileName + ? null + : IconButton( + icon: const PhosphorIcon(PhosphorIconsLight.arrowCounterClockwise), + tooltip: LeapLocalizations.of(context).reset, + onPressed: () => context + .read() + .changeDefaultFileName(kDefaultFileName), + ), + ); +} + +Future _changeDefaultFileName( + BuildContext context, + String initialValue, +) async { + final result = await showFileNamePatternDialog( + context, + initialValue: initialValue, + label: AppLocalizations.of(context).defaultFileName, + ); + if (result != null && context.mounted) { + await context.read().changeDefaultFileName(result); + } +} + Future changeDataDirectory(BuildContext context) async { try { final settingsCubit = context.read(); diff --git a/app/lib/settings/pages/data.dart b/app/lib/settings/pages/data.dart index a19da56b180a..041099363f62 100644 --- a/app/lib/settings/pages/data.dart +++ b/app/lib/settings/pages/data.dart @@ -33,6 +33,11 @@ final _dataSettingsPage = SettingsLeapPage( enabled: (context, state) => !kIsWeb, builder: buildDataDirectorySetting, ), + SettingsLeapCustomSetting( + displayName: (context) => + AppLocalizations.of(context).defaultFileName, + builder: buildDefaultFileNameSetting, + ), SettingsLeapActionSetting( displayName: (context) => AppLocalizations.of(context).templates, icon: PhosphorIconsLight.file, diff --git a/app/lib/widgets/file_name_pattern_field.dart b/app/lib/widgets/file_name_pattern_field.dart new file mode 100644 index 000000000000..14af0b65114d --- /dev/null +++ b/app/lib/widgets/file_name_pattern_field.dart @@ -0,0 +1,188 @@ +import 'package:butterfly/api/save.dart'; +import 'package:butterfly/src/generated/i18n/app_localizations.dart'; +import 'package:flutter/material.dart'; +import 'package:material_leap/material_leap.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +Future showFileNamePatternDialog( + BuildContext context, { + required String initialValue, + required String label, +}) { + final formKey = GlobalKey(); + var value = initialValue; + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(label), + scrollable: true, + constraints: const BoxConstraints(maxWidth: 420), + content: Form( + key: formKey, + child: FileNamePatternField( + initialValue: initialValue, + label: label, + autofocus: true, + onChanged: (newValue) => value = newValue, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), + ), + ElevatedButton( + onPressed: () { + if (!(formKey.currentState?.validate() ?? false)) return; + Navigator.of(context).pop(value.trim()); + }, + child: Text(MaterialLocalizations.of(context).saveButtonLabel), + ), + ], + ), + ); +} + +class FileNamePatternField extends StatefulWidget { + const FileNamePatternField({ + super.key, + required this.initialValue, + required this.label, + required this.onChanged, + this.autofocus = false, + }); + + final String initialValue; + final String label; + final ValueChanged onChanged; + final bool autofocus; + + @override + State createState() => _FileNamePatternFieldState(); +} + +class _FileNamePatternFieldState extends State { + late String _value = widget.initialValue; + + String? _validate(String? value) { + final pattern = value?.trim() ?? ''; + if (pattern.isEmpty) return null; + try { + final resolved = resolveTemplateFileName( + pattern, + DateTime(2000, 12, 31, 23, 59, 58), + ); + return defaultFileNameValidator(context)(null)(resolved); + } on FormatException { + return LeapLocalizations.of(context).invalidName; + } + } + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context); + final preview = previewTemplateFileName(_value); + final exampleDate = DateTime.now(); + final dateExample = resolveTemplateFileName('{date}', exampleDate); + final timeExample = resolveTemplateFileName('{time}', exampleDate); + final colorScheme = Theme.of(context).colorScheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextFormField( + initialValue: widget.initialValue, + autofocus: widget.autofocus, + onChanged: (value) { + setState(() => _value = value); + widget.onChanged(value); + }, + validator: _validate, + decoration: InputDecoration(labelText: widget.label, filled: true), + ), + const SizedBox(height: 8), + DecoratedBox( + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PhosphorIcon( + PhosphorIconsLight.info, + size: 20, + color: colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + localizations.templateFileNameDescription( + templateDateFormatExample, + templateTimeFormatExample, + ), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 10), + _PlaceholderExample( + placeholder: templateDateFormatExample, + result: dateExample, + ), + const SizedBox(height: 6), + _PlaceholderExample( + placeholder: templateTimeFormatExample, + result: timeExample, + ), + if (preview != null) ...[ + const Divider(height: 20), + Text( + '${localizations.preview}: $preview', + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith(fontWeight: FontWeight.w600), + ), + ], + ], + ), + ), + ], + ), + ), + ), + ], + ); + } +} + +class _PlaceholderExample extends StatelessWidget { + const _PlaceholderExample({required this.placeholder, required this.result}); + + final String placeholder; + final String result; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return DecoratedBox( + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(6), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), + child: Text( + '$placeholder → $result', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontFamily: 'monospace', + color: colorScheme.onSurface, + ), + ), + ), + ); + } +} diff --git a/app/test/actions/new_test.dart b/app/test/actions/new_test.dart index f8b5ef1c7f8a..05b2c5c1b05d 100644 --- a/app/test/actions/new_test.dart +++ b/app/test/actions/new_test.dart @@ -96,9 +96,12 @@ void main() { expect(saved?.getMetadata()?.name, 'Journal entry'); }); - testWidgets('template without a file name keeps the document unsaved', ( + testWidgets('settings file name is used when template has none', ( tester, ) async { + when( + () => settingsCubit.state, + ).thenReturn(const ButterflySettings(defaultFileName: 'Default note')); var template = NoteData(Archive()); template = template.setMetadata( const FileMetadata( @@ -113,8 +116,30 @@ void main() { await tester.pumpAndSettle(); expect(find.text('Opened'), findsOneWidget); - expect(openedPath, 'journals'); - expect((openedData as NoteData).getMetadata()?.name, isEmpty); - expect(await fileSystem.buildDocumentSystem().getAsset('journals'), isNull); + expect(openedPath, endsWith('journals/Default note.bfly')); + expect((openedData as NoteData).getMetadata()?.name, 'Default note'); + }); + + testWidgets('template file name overrides the settings default', ( + tester, + ) async { + when( + () => settingsCubit.state, + ).thenReturn(const ButterflySettings(defaultFileName: 'Default note')); + var template = NoteData(Archive()); + template = template.setMetadata( + const FileMetadata( + type: NoteFileType.template, + directory: 'journals', + fileName: 'Template note', + ), + ); + + await tester.pumpWidget(buildApp(template)); + await tester.tap(find.text('Create')); + await tester.pumpAndSettle(); + + expect(openedPath, endsWith('journals/Template note.bfly')); + expect((openedData as NoteData).getMetadata()?.name, 'Template note'); }); } diff --git a/app/test/api/save_test.dart b/app/test/api/save_test.dart index 3898cb2e0f0c..050060222814 100644 --- a/app/test/api/save_test.dart +++ b/app/test/api/save_test.dart @@ -12,4 +12,31 @@ void main() { test('uses a fallback for empty export filenames', () { expect(sanitizeExportFileName(' '), 'output'); }); + + test('resolves template filename date and time placeholders', () { + expect( + resolveTemplateFileName( + 'Daily {date:dd.MM.yyyy} {time:HH-mm-ss}', + DateTime(2026, 7, 10, 9, 8, 7), + ), + 'Daily 10.07.2026 09-08-07', + ); + }); + + test('resolves simple user-facing date and time placeholders', () { + expect( + resolveTemplateFileName( + 'Note {date} {time}', + DateTime(2026, 7, 10, 9, 8), + ), + 'Note 2026-07-10 09-08', + ); + }); + + test('rejects incomplete template filename placeholders', () { + expect( + () => resolveTemplateFileName('Daily {date:}', DateTime(2026)), + throwsFormatException, + ); + }); } diff --git a/app/test/cubits/settings_test.dart b/app/test/cubits/settings_test.dart index b4a3b5164bfd..4bec3b263f26 100644 --- a/app/test/cubits/settings_test.dart +++ b/app/test/cubits/settings_test.dart @@ -6,6 +6,41 @@ import 'package:shared_preferences/shared_preferences.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); + test('uses a visible default file name pattern', () async { + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + + expect( + ButterflySettings.fromPrefs(prefs).defaultFileName, + kDefaultFileName, + ); + expect(const ButterflySettings().defaultFileName, kDefaultFileName); + }); + + test('persists the default file name pattern', () async { + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + final cubit = SettingsCubit(prefs); + + await cubit.changeDefaultFileName(' Notes {date} '); + + expect(cubit.state.defaultFileName, 'Notes {date}'); + expect(prefs.getString('default_file_name'), 'Notes {date}'); + expect(ButterflySettings.fromPrefs(prefs).defaultFileName, 'Notes {date}'); + }); + + test('resetting an empty file name restores the default pattern', () async { + SharedPreferences.setMockInitialValues({}); + final prefs = await SharedPreferences.getInstance(); + final cubit = SettingsCubit(prefs); + + await cubit.changeDefaultFileName('Custom'); + await cubit.changeDefaultFileName(' '); + + expect(cubit.state.defaultFileName, kDefaultFileName); + expect(prefs.getString('default_file_name'), kDefaultFileName); + }); + group('SettingsCubit recent history', () { test( 'deduplicates matching locations with and without leading slash', diff --git a/app/test/widgets/file_name_pattern_field_test.dart b/app/test/widgets/file_name_pattern_field_test.dart new file mode 100644 index 000000000000..19073eeefed1 --- /dev/null +++ b/app/test/widgets/file_name_pattern_field_test.dart @@ -0,0 +1,51 @@ +import 'package:butterfly/src/generated/i18n/app_localizations.dart'; +import 'package:butterfly/widgets/file_name_pattern_field.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('filename pattern dialog shows full help and a live preview', ( + tester, + ) async { + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Builder( + builder: (context) => Scaffold( + body: TextButton( + onPressed: () => showFileNamePatternDialog( + context, + initialValue: '{date}', + label: 'Default file name', + ), + child: const Text('Configure'), + ), + ), + ), + ), + ); + + await tester.tap(find.text('Configure')); + await tester.pumpAndSettle(); + + final helpFinder = find.textContaining( + 'New documents are saved automatically with this name.', + ); + expect(helpFinder, findsOneWidget); + final help = tester.widget(helpFinder); + expect(help.maxLines, isNull); + expect(help.overflow, isNull); + expect(find.textContaining('{date} →'), findsOneWidget); + expect(find.textContaining('{time} →'), findsOneWidget); + expect(find.textContaining('Preview:'), findsOneWidget); + + final dialog = tester.widget(find.byType(AlertDialog)); + expect(dialog.constraints?.maxWidth, 420); + + await tester.enterText(find.byType(TextFormField), 'Notes {date}'); + await tester.pump(); + + expect(find.textContaining('Preview: Notes '), findsOneWidget); + }); +} From 6f9fde2caebd005fd69e1d4c7c66cb0669eac6c8 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 14 Jul 2026 14:15:09 +0200 Subject: [PATCH 083/117] Fix unname files, disable autosave for quick start --- app/lib/actions/new.dart | 44 +++-- app/lib/api/file_system.dart | 13 ++ app/lib/cubits/settings.dart | 10 +- app/lib/dialogs/template.dart | 20 +- app/lib/l10n/app_en.arb | 2 +- app/lib/settings/data.dart | 1 + app/lib/views/home/start.dart | 8 +- app/lib/widgets/file_name_pattern_field.dart | 10 +- app/test/actions/new_test.dart | 184 +++++++++++++++++- app/test/cubits/settings_test.dart | 21 +- .../widgets/file_name_pattern_field_test.dart | 2 +- 11 files changed, 266 insertions(+), 49 deletions(-) diff --git a/app/lib/actions/new.dart b/app/lib/actions/new.dart index df9d141cdc90..1e746a9e1503 100644 --- a/app/lib/actions/new.dart +++ b/app/lib/actions/new.dart @@ -3,6 +3,7 @@ import 'package:butterfly/api/save.dart'; import 'package:butterfly/bloc/document_bloc.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/models/defaults.dart'; +import 'package:butterfly/services/logger.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -57,32 +58,31 @@ class NewAction extends Action { final template = await templateSystem.getDefaultFile( templateSystem.storage?.defaults['template'] ?? settings.defaultTemplate, ); - await openNewDocument(context, true, template); + await openNewDocument(context, true, template: template, autoSave: false); } } Future openNewDocument( BuildContext context, - bool replace, [ + bool replace, { NoteData? template, String? remote, Area? initialArea, -]) async { + bool autoSave = true, +}) async { NoteData? document; String? path; var targetRemote = remote; if (template != null) { final settings = context.read().state; final templatePattern = template.getMetadata()?.fileName.trim() ?? ''; - final fileNamePattern = templatePattern.isNotEmpty - ? templatePattern - : settings.defaultFileName.trim(); var documentName = ''; - if (fileNamePattern.isNotEmpty) { + var shouldAutoSave = autoSave; + if (shouldAutoSave && templatePattern.isNotEmpty) { try { - documentName = resolveTemplateFileName(fileNamePattern, DateTime.now()); + documentName = resolveTemplateFileName(templatePattern, DateTime.now()); } on FormatException { - // Invalid patterns from imported templates should still open unsaved. + shouldAutoSave = false; } } document = template.createDocument(name: documentName); @@ -95,19 +95,27 @@ Future openNewDocument( final metadata = document.getMetadata(); if (metadata != null) { path = metadata.directory; - if (fileNamePattern.isNotEmpty && metadata.name.isNotEmpty) { + if (shouldAutoSave) { final storage = settings.getRemote(targetRemote); final fileSystem = context .read() .buildDocumentSystem(storage); - final created = await fileSystem.createFileWithName( - directory: path, - name: metadata.name, - suffix: '.bfly', - document.toFile(), - ); - path = created.path; - targetRemote = created.remote; + try { + final created = await fileSystem.createFileWithName( + directory: path, + name: metadata.name, + suffix: '.bfly', + document.toFile(), + ); + path = created.path; + targetRemote = storage?.identifier ?? ''; + } catch (error, stackTrace) { + talker.warning( + 'Failed to create document from filename pattern', + error, + stackTrace, + ); + } } } } diff --git a/app/lib/api/file_system.dart b/app/lib/api/file_system.dart index 73a7fefe83ac..f963d1d27238 100644 --- a/app/lib/api/file_system.dart +++ b/app/lib/api/file_system.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:archive/archive.dart'; +import 'package:butterfly/api/save.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/models/defaults.dart'; import 'package:butterfly/models/persisted_document_state.dart'; @@ -131,6 +132,18 @@ class ButterflyFileSystem { database: _database, databaseVersion: _databaseVersion, onDatabaseUpgrade: _upgradeDatabase, + getUnnamed: () { + final pattern = settingsCubit.state.defaultFileName.trim(); + try { + final resolved = resolveTemplateFileName( + pattern.isEmpty ? kDefaultFileName : pattern, + DateTime.now(), + ); + return resolved.isEmpty ? FileSystemConfig.unnamedDate() : resolved; + } on FormatException { + return FileSystemConfig.unnamedDate(); + } + }, ), _templateConfig = FileSystemConfig( passwordStorage: passwordStorage, diff --git a/app/lib/cubits/settings.dart b/app/lib/cubits/settings.dart index cb341e0af834..2eb46cc08441 100644 --- a/app/lib/cubits/settings.dart +++ b/app/lib/cubits/settings.dart @@ -539,6 +539,7 @@ sealed class ButterflySettings with _$ButterflySettings, LeapSettings { _$ButterflySettingsFromJson(json); factory ButterflySettings.fromPrefs(SharedPreferences prefs) { + final storedDefaultFileName = prefs.getString('default_file_name')?.trim(); final connections = prefs .getStringList('connections') @@ -638,7 +639,9 @@ sealed class ButterflySettings with _$ButterflySettings, LeapSettings { .toList() ?? [], defaultTemplate: prefs.getString('default_template') ?? '', - defaultFileName: prefs.getString('default_file_name') ?? kDefaultFileName, + defaultFileName: storedDefaultFileName?.isNotEmpty ?? false + ? storedDefaultFileName! + : kDefaultFileName, toolbarPosition: prefs.containsKey('toolbar_position') ? _enumByNameOr( ToolbarPosition.values, @@ -1418,11 +1421,10 @@ class SettingsCubit extends Cubit } Future changeDefaultFileName(String pattern) { + final normalized = pattern.trim(); emit( state.copyWith( - defaultFileName: pattern.trim().isEmpty - ? kDefaultFileName - : pattern.trim(), + defaultFileName: normalized.isEmpty ? kDefaultFileName : normalized, ), ); return save(); diff --git a/app/lib/dialogs/template.dart b/app/lib/dialogs/template.dart index f7559c9c9fd5..c49a6dbbb8c3 100644 --- a/app/lib/dialogs/template.dart +++ b/app/lib/dialogs/template.dart @@ -221,9 +221,9 @@ class _TemplateDialogState extends State { openNewDocument( context, widget.bloc != null, - detailsTemplate, - _templateSystem.storage?.identifier, - area, + template: detailsTemplate, + remote: _templateSystem.storage?.identifier, + initialArea: area, ); }, ), @@ -317,9 +317,9 @@ class _TemplateDialogState extends State { openNewDocument( context, widget.bloc != null, - template.data!, - _templateSystem.storage?.identifier, - area, + template: template.data!, + remote: _templateSystem.storage?.identifier, + initialArea: area, ); }, ), @@ -1113,8 +1113,8 @@ class _TemplateItem extends StatelessWidget { () => openNewDocument( context, bloc != null, - template, - fileSystem.storage?.identifier, + template: template, + remote: fileSystem.storage?.identifier, ), ); } @@ -1204,8 +1204,8 @@ class _TemplateCard extends StatelessWidget { () => openNewDocument( context, bloc != null, - template, - fileSystem.storage?.identifier, + template: template, + remote: fileSystem.storage?.identifier, ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index cb1952d3a78c..3a4d5ced0754 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -352,7 +352,7 @@ "createTemplateContent": "Do you really want to create a template from this document? The original document will get deleted.", "fileName": "File name", "defaultFileName": "Default file name", - "templateFileNameDescription": "New documents are saved automatically with this name. Combine your own text with {dateExample} and {timeExample}. Characters that are not allowed in file names will be rejected.", + "templateFileNameDescription": "Documents opened from the template picker are saved automatically with this name. Templates without a name use the default configured in Data settings. Combine your own text with {dateExample} and {timeExample}. Characters that are not allowed in file names will be rejected.", "@templateFileNameDescription": { "placeholders": { "dateExample": { diff --git a/app/lib/settings/data.dart b/app/lib/settings/data.dart index 915f7a2e5e13..183189fe06ef 100644 --- a/app/lib/settings/data.dart +++ b/app/lib/settings/data.dart @@ -97,6 +97,7 @@ Future _changeDefaultFileName( context, initialValue: initialValue, label: AppLocalizations.of(context).defaultFileName, + allowEmpty: false, ); if (result != null && context.mounted) { await context.read().changeDefaultFileName(result); diff --git a/app/lib/views/home/start.dart b/app/lib/views/home/start.dart index 12b3f6ea05b0..82b9f9e52dc1 100644 --- a/app/lib/views/home/start.dart +++ b/app/lib/views/home/start.dart @@ -182,7 +182,13 @@ class _QuickstartHomeViewState extends State<_QuickstartHomeView> { metadata: metadata, thumbnail: thumbnail, onTap: () async { - await openNewDocument(context, false, data, e.remote); + await openNewDocument( + context, + false, + template: data, + remote: e.remote, + autoSave: false, + ); widget.onReload(); }, ); diff --git a/app/lib/widgets/file_name_pattern_field.dart b/app/lib/widgets/file_name_pattern_field.dart index 14af0b65114d..172485a80d13 100644 --- a/app/lib/widgets/file_name_pattern_field.dart +++ b/app/lib/widgets/file_name_pattern_field.dart @@ -8,6 +8,7 @@ Future showFileNamePatternDialog( BuildContext context, { required String initialValue, required String label, + bool allowEmpty = true, }) { final formKey = GlobalKey(); var value = initialValue; @@ -23,6 +24,7 @@ Future showFileNamePatternDialog( initialValue: initialValue, label: label, autofocus: true, + allowEmpty: allowEmpty, onChanged: (newValue) => value = newValue, ), ), @@ -50,12 +52,14 @@ class FileNamePatternField extends StatefulWidget { required this.label, required this.onChanged, this.autofocus = false, + this.allowEmpty = true, }); final String initialValue; final String label; final ValueChanged onChanged; final bool autofocus; + final bool allowEmpty; @override State createState() => _FileNamePatternFieldState(); @@ -66,7 +70,11 @@ class _FileNamePatternFieldState extends State { String? _validate(String? value) { final pattern = value?.trim() ?? ''; - if (pattern.isEmpty) return null; + if (pattern.isEmpty) { + return widget.allowEmpty + ? null + : LeapLocalizations.of(context).invalidName; + } try { final resolved = resolveTemplateFileName( pattern, diff --git a/app/test/actions/new_test.dart b/app/test/actions/new_test.dart index 05b2c5c1b05d..38799c6c8b65 100644 --- a/app/test/actions/new_test.dart +++ b/app/test/actions/new_test.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:archive/archive.dart'; import 'package:butterfly/actions/new.dart'; import 'package:butterfly/api/file_system.dart'; @@ -12,6 +14,27 @@ import 'package:mocktail/mocktail.dart'; import '../helpers/mocks.dart'; +class _MockDocumentFileSystem extends Mock implements DocumentFileSystem {} + +class _TrackingButterflyFileSystem extends MockButterflyFileSystem { + _TrackingButterflyFileSystem({ + required super.settingsCubit, + required this.documentSystem, + }); + + final DocumentFileSystem documentSystem; + ExternalStorage? lastStorage; + + @override + DocumentFileSystem buildDocumentSystem([ + ExternalStorage? storage, + bool forceRecreate = false, + ]) { + lastStorage = storage; + return documentSystem; + } +} + void main() { late MockSettingsCubit settingsCubit; late MockButterflyFileSystem fileSystem; @@ -20,6 +43,8 @@ void main() { String? openedRemote; Object? openedData; + setUpAll(() => registerFallbackValue(NoteFile(Uint8List(0)))); + setUp(() { settingsCubit = MockSettingsCubit(); fileSystem = MockButterflyFileSystem(settingsCubit: settingsCubit); @@ -33,7 +58,7 @@ void main() { tearDown(() => router.dispose()); - Widget buildApp(NoteData template) { + Widget buildApp(NoteData template, {String? remote, bool autoSave = true}) { router = GoRouter( initialLocation: '/', routes: [ @@ -41,7 +66,13 @@ void main() { path: '/', builder: (context, state) => Scaffold( body: TextButton( - onPressed: () => openNewDocument(context, true, template), + onPressed: () => openNewDocument( + context, + true, + template: template, + remote: remote, + autoSave: autoSave, + ), child: const Text('Create'), ), ), @@ -96,12 +127,28 @@ void main() { expect(saved?.getMetadata()?.name, 'Journal entry'); }); - testWidgets('settings file name is used when template has none', ( + testWidgets('template without a name delegates to the file system default', ( tester, ) async { + final documentSystem = _MockDocumentFileSystem(); + fileSystem = _TrackingButterflyFileSystem( + settingsCubit: settingsCubit, + documentSystem: documentSystem, + ); when( - () => settingsCubit.state, - ).thenReturn(const ButterflySettings(defaultFileName: 'Default note')); + () => documentSystem.createFileWithName( + any(), + directory: any(named: 'directory'), + name: '', + suffix: any(named: 'suffix'), + ), + ).thenAnswer((invocation) async { + final data = invocation.positionalArguments.first as NoteFile; + return FileSystemFile( + AssetLocation.local('journals/Default note.bfly'), + data: data, + ); + }); var template = NoteData(Archive()); template = template.setMetadata( const FileMetadata( @@ -117,7 +164,15 @@ void main() { expect(find.text('Opened'), findsOneWidget); expect(openedPath, endsWith('journals/Default note.bfly')); - expect((openedData as NoteData).getMetadata()?.name, 'Default note'); + expect((openedData as NoteData).getMetadata()?.name, isEmpty); + verify( + () => documentSystem.createFileWithName( + any(), + directory: 'journals', + name: '', + suffix: '.bfly', + ), + ).called(1); }); testWidgets('template file name overrides the settings default', ( @@ -142,4 +197,121 @@ void main() { expect(openedPath, endsWith('journals/Template note.bfly')); expect((openedData as NoteData).getMetadata()?.name, 'Template note'); }); + + testWidgets('remote template keeps the selected remote identifier', ( + tester, + ) async { + const remote = DavRemoteStorage( + name: 'remote-test', + username: 'user', + url: 'https://example.com', + ); + final documentSystem = _MockDocumentFileSystem(); + final trackingFileSystem = _TrackingButterflyFileSystem( + settingsCubit: settingsCubit, + documentSystem: documentSystem, + ); + fileSystem = trackingFileSystem; + when( + () => settingsCubit.state, + ).thenReturn(const ButterflySettings(connections: [remote])); + when( + () => documentSystem.createFileWithName( + any(), + directory: any(named: 'directory'), + name: any(named: 'name'), + suffix: any(named: 'suffix'), + ), + ).thenAnswer((invocation) async { + final data = invocation.positionalArguments.first as NoteFile; + return FileSystemFile( + AssetLocation.local('journals/Remote note.bfly'), + data: data, + ); + }); + var template = NoteData(Archive()); + template = template.setMetadata( + const FileMetadata( + type: NoteFileType.template, + directory: 'journals', + fileName: 'Remote note', + ), + ); + + await tester.pumpWidget(buildApp(template, remote: remote.identifier)); + await tester.tap(find.text('Create')); + await tester.pumpAndSettle(); + + expect(find.text('Opened'), findsOneWidget); + expect(openedPath, 'journals/Remote note.bfly'); + expect(openedRemote, remote.identifier); + expect(trackingFileSystem.lastStorage, same(remote)); + }); + + testWidgets('write failure still opens the document unsaved', (tester) async { + final documentSystem = _MockDocumentFileSystem(); + fileSystem = _TrackingButterflyFileSystem( + settingsCubit: settingsCubit, + documentSystem: documentSystem, + ); + when( + () => documentSystem.createFileWithName( + any(), + directory: any(named: 'directory'), + name: any(named: 'name'), + suffix: any(named: 'suffix'), + ), + ).thenThrow(StateError('Storage unavailable')); + var template = NoteData(Archive()); + template = template.setMetadata( + const FileMetadata( + type: NoteFileType.template, + directory: 'journals', + fileName: 'Fallback note', + ), + ); + + await tester.pumpWidget(buildApp(template)); + await tester.tap(find.text('Create')); + await tester.pumpAndSettle(); + + expect(find.text('Opened'), findsOneWidget); + expect(openedPath, 'journals'); + expect(openedRemote, isNull); + expect((openedData as NoteData).getMetadata()?.name, 'Fallback note'); + }); + + testWidgets('quick start ignores automatic filename patterns', ( + tester, + ) async { + final documentSystem = _MockDocumentFileSystem(); + fileSystem = _TrackingButterflyFileSystem( + settingsCubit: settingsCubit, + documentSystem: documentSystem, + ); + var template = NoteData(Archive()); + template = template.setMetadata( + const FileMetadata( + type: NoteFileType.template, + directory: 'journals', + fileName: 'Template note', + ), + ); + + await tester.pumpWidget(buildApp(template, autoSave: false)); + await tester.tap(find.text('Create')); + await tester.pumpAndSettle(); + + expect(find.text('Opened'), findsOneWidget); + expect(openedPath, 'journals'); + expect((openedData as NoteData).getMetadata()?.name, isEmpty); + verifyNever( + () => documentSystem.createFileWithName( + any(), + directory: any(named: 'directory'), + name: any(named: 'name'), + suffix: any(named: 'suffix'), + ), + ); + }); } diff --git a/app/test/cubits/settings_test.dart b/app/test/cubits/settings_test.dart index 4bec3b263f26..37850555c84f 100644 --- a/app/test/cubits/settings_test.dart +++ b/app/test/cubits/settings_test.dart @@ -6,15 +6,12 @@ import 'package:shared_preferences/shared_preferences.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - test('uses a visible default file name pattern', () async { + test('uses the date placeholder as the default file name', () async { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); - expect( - ButterflySettings.fromPrefs(prefs).defaultFileName, - kDefaultFileName, - ); - expect(const ButterflySettings().defaultFileName, kDefaultFileName); + expect(ButterflySettings.fromPrefs(prefs).defaultFileName, '{date}'); + expect(const ButterflySettings().defaultFileName, '{date}'); }); test('persists the default file name pattern', () async { @@ -29,7 +26,7 @@ void main() { expect(ButterflySettings.fromPrefs(prefs).defaultFileName, 'Notes {date}'); }); - test('resetting an empty file name restores the default pattern', () async { + test('an empty file name restores the default pattern', () async { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); final cubit = SettingsCubit(prefs); @@ -41,6 +38,16 @@ void main() { expect(prefs.getString('default_file_name'), kDefaultFileName); }); + test('normalizes an empty persisted file name to the default', () async { + SharedPreferences.setMockInitialValues({'default_file_name': ' '}); + final prefs = await SharedPreferences.getInstance(); + + expect( + ButterflySettings.fromPrefs(prefs).defaultFileName, + kDefaultFileName, + ); + }); + group('SettingsCubit recent history', () { test( 'deduplicates matching locations with and without leading slash', diff --git a/app/test/widgets/file_name_pattern_field_test.dart b/app/test/widgets/file_name_pattern_field_test.dart index 19073eeefed1..4a6c6241d9ef 100644 --- a/app/test/widgets/file_name_pattern_field_test.dart +++ b/app/test/widgets/file_name_pattern_field_test.dart @@ -30,7 +30,7 @@ void main() { await tester.pumpAndSettle(); final helpFinder = find.textContaining( - 'New documents are saved automatically with this name.', + 'Documents opened from the template picker are saved automatically', ); expect(helpFinder, findsOneWidget); final help = tester.widget(helpFinder); From b1edb206546ec8d60e2f35563d14ef46e7bc29d3 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 14 Jul 2026 00:09:23 +0200 Subject: [PATCH 084/117] Add native linux titlebar --- app/lib/api/window.dart | 25 ++++++++++ app/lib/cubits/settings.dart | 16 +++--- app/lib/main.dart | 7 ++- app/lib/setup.dart | 15 +++--- app/lib/setup_web.dart | 4 +- app/linux/runner/my_application.cc | 79 ++++++++++++++++++++++++++++-- 6 files changed, 118 insertions(+), 28 deletions(-) create mode 100644 app/lib/api/window.dart diff --git a/app/lib/api/window.dart b/app/lib/api/window.dart new file mode 100644 index 000000000000..626f05c2d8e4 --- /dev/null +++ b/app/lib/api/window.dart @@ -0,0 +1,25 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:window_manager/window_manager.dart'; + +const _windowChannel = MethodChannel('linwood.dev/butterfly/window'); + +Future applyNativeTitleBar(bool nativeTitleBar) async { + if (kIsWeb) return; + + // This still handles GtkHeaderBar on GNOME and the regular desktop + // implementations on Windows, macOS and X11. + await windowManager.setTitleBarStyle( + nativeTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden, + windowButtonVisibility: nativeTitleBar, + ); + + if (defaultTargetPlatform == TargetPlatform.linux) { + // gtk_window_set_decorated() is ineffective in GTK 3's Wayland backend. + // Ask GDK to negotiate KDE's server/client decoration mode directly. + await _windowChannel.invokeMethod( + 'setNativeTitleBar', + nativeTitleBar, + ); + } +} diff --git a/app/lib/cubits/settings.dart b/app/lib/cubits/settings.dart index 2eb46cc08441..70f20ebbaa18 100644 --- a/app/lib/cubits/settings.dart +++ b/app/lib/cubits/settings.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:butterfly/api/file_system.dart'; +import 'package:butterfly/api/window.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; @@ -1352,20 +1353,15 @@ class SettingsCubit extends Cubit return save(); } - void setNativeTitleBar([bool? value]) { + Future setNativeTitleBar([bool? value]) async { if (kIsWeb || !isWindow) return; - windowManager.setTitleBarStyle( - (value ?? state.nativeTitleBar) - ? TitleBarStyle.normal - : TitleBarStyle.hidden, - windowButtonVisibility: state.nativeTitleBar, - ); + await applyNativeTitleBar(value ?? state.nativeTitleBar); } - Future changeNativeTitleBar(bool value, [bool modify = true]) { - if (modify) setNativeTitleBar(value); + Future changeNativeTitleBar(bool value, [bool modify = true]) async { + if (modify) await setNativeTitleBar(value); emit(state.copyWith(nativeTitleBar: value)); - return save(); + await save(); } Future changeSyncMode(SyncMode syncMode) { diff --git a/app/lib/main.dart b/app/lib/main.dart index 2b6111143b14..a9c672109ec6 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -44,13 +44,13 @@ Future main([List args = const []]) async { talker.info('App started'); usePathUrlStrategy(); - await setup(); + final prefs = await SharedPreferences.getInstance(); + final settingsCubit = SettingsCubit(prefs); + await setup(nativeTitleBar: settingsCubit.state.nativeTitleBar); var initialLocation = '/'; final argParser = ArgParser(); argParser.addOption('path', abbr: 'p'); final result = argParser.parse(args); - final prefs = await SharedPreferences.getInstance(); - final settingsCubit = SettingsCubit(prefs); Object? initialExtra; if (result.arguments.isNotEmpty && !kIsWeb) { var path = result.arguments[0].replaceAll('\\', '/'); @@ -323,7 +323,6 @@ class ButterflyApp extends StatelessWidget { if (!kIsWeb && isWindow) { windowManager.waitUntilReadyToShow(null, () async { settingsCubit.setTheme(context); - settingsCubit.setNativeTitleBar(); await windowManager.show(); }); } diff --git a/app/lib/setup.dart b/app/lib/setup.dart index 05ce630036d5..74c098c192b9 100644 --- a/app/lib/setup.dart +++ b/app/lib/setup.dart @@ -1,3 +1,4 @@ +import 'package:butterfly/api/window.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -7,21 +8,21 @@ import 'package:window_manager/window_manager.dart'; import 'main.dart'; -Future setup() async { +Future setup({required bool nativeTitleBar}) async { pdfrxFlutterInitialize(); if (!kIsWeb && isWindow) { await windowManager.ensureInitialized(); - const kWindowOptions = WindowOptions( + const windowOptions = WindowOptions( minimumSize: Size(410, 300), title: applicationName, backgroundColor: Colors.transparent, ); - // Use it only after calling `hiddenWindowAtLaunch` - await windowManager.waitUntilReadyToShow(kWindowOptions).then((_) async { - await windowManager.setResizable(true); - await windowManager.setPreventClose(false); - }); + await windowManager.waitUntilReadyToShow(windowOptions); + + await windowManager.setResizable(true); + await windowManager.setPreventClose(false); + await applyNativeTitleBar(nativeTitleBar); } setupFullScreen(); setupLicenses(); diff --git a/app/lib/setup_web.dart b/app/lib/setup_web.dart index fc67caeab5d3..030fd14f9aea 100644 --- a/app/lib/setup_web.dart +++ b/app/lib/setup_web.dart @@ -3,8 +3,8 @@ import 'package:flutter/services.dart'; import 'setup.dart' as general_setup; import 'embed/action_web.dart' as action; -Future setup() async { +Future setup({required bool nativeTitleBar}) async { await BrowserContextMenu.disableContextMenu(); action.setup(); - await general_setup.setup(); + await general_setup.setup(nativeTitleBar: nativeTitleBar); } diff --git a/app/linux/runner/my_application.cc b/app/linux/runner/my_application.cc index 07506e5a15da..0932b91571f7 100644 --- a/app/linux/runner/my_application.cc +++ b/app/linux/runner/my_application.cc @@ -4,16 +4,75 @@ #ifdef GDK_WINDOWING_X11 #include #endif +#ifdef GDK_WINDOWING_WAYLAND +#include +#endif #include "flutter/generated_plugin_registrant.h" struct _MyApplication { GtkApplication parent_instance; char** dart_entrypoint_arguments; + FlMethodChannel* window_channel; }; G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) +static FlMethodResponse* set_native_title_bar(GtkWindow* window, + FlValue* args) { + if (args == nullptr || fl_value_get_type(args) != FL_VALUE_TYPE_BOOL) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + "invalid-argument", "Expected a boolean nativeTitleBar value.", + nullptr)); + } + GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(window)); + if (gdk_window == nullptr) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + "window-not-realized", "The GTK window has not been realized.", + nullptr)); + } +#ifdef GDK_WINDOWING_WAYLAND + if (GDK_IS_WAYLAND_WINDOW(gdk_window)) { + const gboolean native_title_bar = fl_value_get_bool(args); + if (native_title_bar) { + gdk_wayland_window_announce_ssd(gdk_window); + } else { + gdk_wayland_window_announce_csd(gdk_window); + } + } +#endif + return FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); +} + +static void window_method_call_cb(FlMethodChannel* channel, + FlMethodCall* method_call, + gpointer user_data) { + GtkWindow* window = GTK_WINDOW(user_data); + g_autoptr(FlMethodResponse) response = nullptr; + if (g_strcmp0(fl_method_call_get_name(method_call), "setNativeTitleBar") == + 0) { + response = set_native_title_bar(window, fl_method_call_get_args(method_call)); + } else { + response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); + } + g_autoptr(GError) error = nullptr; + if (!fl_method_call_respond(method_call, response, &error)) { + g_warning("Failed to send window method response: %s", error->message); + } +} + +static void create_window_channel(MyApplication* self, + FlView* view, + GtkWindow* window) { + FlEngine* engine = fl_view_get_engine(view); + FlBinaryMessenger* messenger = fl_engine_get_binary_messenger(engine); + g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); + self->window_channel = fl_method_channel_new( + messenger, "linwood.dev/butterfly/window", FL_METHOD_CODEC(codec)); + fl_method_channel_set_method_call_handler( + self->window_channel, window_method_call_cb, window, nullptr); +} + // Called when first Flutter frame received. static void first_frame_cb(MyApplication* self, FlView *view) { @@ -33,16 +92,24 @@ static void my_application_activate(GApplication* application) { // in case the window manager does more exotic layout, e.g. tiling. // If running on Wayland assume the header bar will work (may need changing // if future cases occur). - gboolean use_header_bar = TRUE; + gboolean use_header_bar = FALSE; #ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); + GdkScreen* screen = gtk_window_get_screen(GTK_WINDOW(window)); if (GDK_IS_X11_SCREEN(screen)) { const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { - use_header_bar = FALSE; + if (g_strcmp0(wm_name, "GNOME Shell") == 0) { + use_header_bar = TRUE; } - } + } else #endif + { + const gchar* current_desktop = g_getenv("XDG_CURRENT_DESKTOP"); + if (current_desktop != nullptr) { + g_auto(GStrv) desktops = g_strsplit(current_desktop, ":", -1); + use_header_bar = g_strv_contains( + reinterpret_cast(desktops), "GNOME"); + } + } if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); @@ -73,6 +140,7 @@ static void my_application_activate(GApplication* application) { fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + create_window_channel(self, view, window); gtk_widget_grab_focus(GTK_WIDGET(view)); } @@ -116,6 +184,7 @@ static void my_application_shutdown(GApplication* application) { // Implements GObject::dispose. static void my_application_dispose(GObject* object) { MyApplication* self = MY_APPLICATION(object); + g_clear_object(&self->window_channel); g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); G_OBJECT_CLASS(my_application_parent_class)->dispose(object); } From 8333e0ae9f5a5030f46eed9e9e298a75cb4658c9 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 14 Jul 2026 18:18:33 +0200 Subject: [PATCH 085/117] Add custom fonts, closes #1011 --- api/lib/src/models/data.dart | 15 +- api/lib/src/models/text.dart | 4 +- api/lib/src/models/text.freezed.dart | 40 +++-- api/lib/src/models/text.g.dart | 8 + api/test/data_test.dart | 38 +++++ app/lib/api/open.dart | 15 ++ app/lib/dialogs/packs/fonts.dart | 91 ++++++++++ app/lib/dialogs/packs/pack.dart | 5 +- app/lib/dialogs/packs/styles/general.dart | 17 ++ app/lib/main.dart | 13 +- app/lib/renderers/elements/text.dart | 30 +++- app/lib/renderers/renderer.dart | 1 + app/lib/services/font.dart | 106 ++++++++++++ app/lib/setup.dart | 2 +- app/lib/theme.dart | 4 +- app/lib/views/main.dart | 6 + app/lib/views/toolbar/label.dart | 70 +++++--- app/lib/visualizer/text.dart | 32 +++- app/lib/widgets/font_style_field.dart | 157 ++++++++++++++++++ app/pubspec.yaml | 2 +- .../views/project_page_lifecycle_test.dart | 4 + 21 files changed, 599 insertions(+), 61 deletions(-) create mode 100644 app/lib/dialogs/packs/fonts.dart create mode 100644 app/lib/services/font.dart create mode 100644 app/lib/widgets/font_style_field.dart diff --git a/api/lib/src/models/data.dart b/api/lib/src/models/data.dart index d50ff7bbd4fa..4d51042f575d 100644 --- a/api/lib/src/models/data.dart +++ b/api/lib/src/models/data.dart @@ -528,7 +528,8 @@ final class NoteData extends NoteDisplay { @useResult Uint8List? getFont(String fontName) => - getAsset('$kFontsArchiveDirectory/$fontName'); + getAsset('$kFontsArchiveDirectory/$fontName') ?? + parent?.getFont(fontName); @useResult Uint8List? getBundledPackData(String packName) => @@ -572,6 +573,18 @@ final class NoteData extends NoteDisplay { ...getAssets(path, recursive), }; + @useResult + Iterable getFonts() => + _getPackAssets('$kFontsArchiveDirectory/', false); + + @useResult + NoteData setFont(String name, Uint8List font) => + setAsset('$kFontsArchiveDirectory/$name', font); + + @useResult + NoteData removeFont(String name) => + removeAsset('$kFontsArchiveDirectory/$name'); + @useResult Iterable getComponents() => _getPackAssets('$kComponentsArchiveDirectory/'); diff --git a/api/lib/src/models/text.dart b/api/lib/src/models/text.dart index e5a0364d553e..7a69656c0b2c 100644 --- a/api/lib/src/models/text.dart +++ b/api/lib/src/models/text.dart @@ -1,12 +1,10 @@ import 'dart:math'; - import 'package:butterfly_api/src/converter/color.dart'; import 'package:collection/collection.dart'; import 'package:dart_leap/dart_leap.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'pack.dart'; - part 'text.freezed.dart'; part 'text.g.dart'; @@ -344,6 +342,8 @@ sealed class TextStyleSheet extends PackAsset with _$TextStyleSheet { const TextStyleSheet._(); const factory TextStyleSheet({ + @Default('Roboto') String fontFamily, + @Default([]) List fontFamilyFallback, @Default({}) Map spanProperties, @Default({}) Map paragraphProperties, }) = _TextStyleSheet; diff --git a/api/lib/src/models/text.freezed.dart b/api/lib/src/models/text.freezed.dart index 5b4abf81ceb5..0428d0c891ac 100644 --- a/api/lib/src/models/text.freezed.dart +++ b/api/lib/src/models/text.freezed.dart @@ -1396,7 +1396,7 @@ $TextParagraphCopyWith<$Res> get paragraph { /// @nodoc mixin _$TextStyleSheet { - Map get spanProperties; Map get paragraphProperties; + String get fontFamily; List get fontFamilyFallback; Map get spanProperties; Map get paragraphProperties; /// Create a copy of TextStyleSheet /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -1409,16 +1409,16 @@ $TextStyleSheetCopyWith get copyWith => _$TextStyleSheetCopyWith @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is TextStyleSheet&&const DeepCollectionEquality().equals(other.spanProperties, spanProperties)&&const DeepCollectionEquality().equals(other.paragraphProperties, paragraphProperties)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is TextStyleSheet&&(identical(other.fontFamily, fontFamily) || other.fontFamily == fontFamily)&&const DeepCollectionEquality().equals(other.fontFamilyFallback, fontFamilyFallback)&&const DeepCollectionEquality().equals(other.spanProperties, spanProperties)&&const DeepCollectionEquality().equals(other.paragraphProperties, paragraphProperties)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(spanProperties),const DeepCollectionEquality().hash(paragraphProperties)); +int get hashCode => Object.hash(runtimeType,fontFamily,const DeepCollectionEquality().hash(fontFamilyFallback),const DeepCollectionEquality().hash(spanProperties),const DeepCollectionEquality().hash(paragraphProperties)); @override String toString() { - return 'TextStyleSheet(spanProperties: $spanProperties, paragraphProperties: $paragraphProperties)'; + return 'TextStyleSheet(fontFamily: $fontFamily, fontFamilyFallback: $fontFamilyFallback, spanProperties: $spanProperties, paragraphProperties: $paragraphProperties)'; } @@ -1429,7 +1429,7 @@ abstract mixin class $TextStyleSheetCopyWith<$Res> { factory $TextStyleSheetCopyWith(TextStyleSheet value, $Res Function(TextStyleSheet) _then) = _$TextStyleSheetCopyWithImpl; @useResult $Res call({ - Map spanProperties, Map paragraphProperties + String fontFamily, List fontFamilyFallback, Map spanProperties, Map paragraphProperties }); @@ -1446,9 +1446,11 @@ class _$TextStyleSheetCopyWithImpl<$Res> /// Create a copy of TextStyleSheet /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? spanProperties = null,Object? paragraphProperties = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? fontFamily = null,Object? fontFamilyFallback = null,Object? spanProperties = null,Object? paragraphProperties = null,}) { return _then(_self.copyWith( -spanProperties: null == spanProperties ? _self.spanProperties : spanProperties // ignore: cast_nullable_to_non_nullable +fontFamily: null == fontFamily ? _self.fontFamily : fontFamily // ignore: cast_nullable_to_non_nullable +as String,fontFamilyFallback: null == fontFamilyFallback ? _self.fontFamilyFallback : fontFamilyFallback // ignore: cast_nullable_to_non_nullable +as List,spanProperties: null == spanProperties ? _self.spanProperties : spanProperties // ignore: cast_nullable_to_non_nullable as Map,paragraphProperties: null == paragraphProperties ? _self.paragraphProperties : paragraphProperties // ignore: cast_nullable_to_non_nullable as Map, )); @@ -1462,9 +1464,17 @@ as Map, @JsonSerializable() class _TextStyleSheet extends TextStyleSheet { - const _TextStyleSheet({final Map spanProperties = const {}, final Map paragraphProperties = const {}}): _spanProperties = spanProperties,_paragraphProperties = paragraphProperties,super._(); + const _TextStyleSheet({this.fontFamily = 'Roboto', final List fontFamilyFallback = const [], final Map spanProperties = const {}, final Map paragraphProperties = const {}}): _fontFamilyFallback = fontFamilyFallback,_spanProperties = spanProperties,_paragraphProperties = paragraphProperties,super._(); factory _TextStyleSheet.fromJson(Map json) => _$TextStyleSheetFromJson(json); +@override@JsonKey() final String fontFamily; + final List _fontFamilyFallback; +@override@JsonKey() List get fontFamilyFallback { + if (_fontFamilyFallback is EqualUnmodifiableListView) return _fontFamilyFallback; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_fontFamilyFallback); +} + final Map _spanProperties; @override@JsonKey() Map get spanProperties { if (_spanProperties is EqualUnmodifiableMapView) return _spanProperties; @@ -1493,16 +1503,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _TextStyleSheet&&const DeepCollectionEquality().equals(other._spanProperties, _spanProperties)&&const DeepCollectionEquality().equals(other._paragraphProperties, _paragraphProperties)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _TextStyleSheet&&(identical(other.fontFamily, fontFamily) || other.fontFamily == fontFamily)&&const DeepCollectionEquality().equals(other._fontFamilyFallback, _fontFamilyFallback)&&const DeepCollectionEquality().equals(other._spanProperties, _spanProperties)&&const DeepCollectionEquality().equals(other._paragraphProperties, _paragraphProperties)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_spanProperties),const DeepCollectionEquality().hash(_paragraphProperties)); +int get hashCode => Object.hash(runtimeType,fontFamily,const DeepCollectionEquality().hash(_fontFamilyFallback),const DeepCollectionEquality().hash(_spanProperties),const DeepCollectionEquality().hash(_paragraphProperties)); @override String toString() { - return 'TextStyleSheet(spanProperties: $spanProperties, paragraphProperties: $paragraphProperties)'; + return 'TextStyleSheet(fontFamily: $fontFamily, fontFamilyFallback: $fontFamilyFallback, spanProperties: $spanProperties, paragraphProperties: $paragraphProperties)'; } @@ -1513,7 +1523,7 @@ abstract mixin class _$TextStyleSheetCopyWith<$Res> implements $TextStyleSheetCo factory _$TextStyleSheetCopyWith(_TextStyleSheet value, $Res Function(_TextStyleSheet) _then) = __$TextStyleSheetCopyWithImpl; @override @useResult $Res call({ - Map spanProperties, Map paragraphProperties + String fontFamily, List fontFamilyFallback, Map spanProperties, Map paragraphProperties }); @@ -1530,9 +1540,11 @@ class __$TextStyleSheetCopyWithImpl<$Res> /// Create a copy of TextStyleSheet /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? spanProperties = null,Object? paragraphProperties = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? fontFamily = null,Object? fontFamilyFallback = null,Object? spanProperties = null,Object? paragraphProperties = null,}) { return _then(_TextStyleSheet( -spanProperties: null == spanProperties ? _self._spanProperties : spanProperties // ignore: cast_nullable_to_non_nullable +fontFamily: null == fontFamily ? _self.fontFamily : fontFamily // ignore: cast_nullable_to_non_nullable +as String,fontFamilyFallback: null == fontFamilyFallback ? _self._fontFamilyFallback : fontFamilyFallback // ignore: cast_nullable_to_non_nullable +as List,spanProperties: null == spanProperties ? _self._spanProperties : spanProperties // ignore: cast_nullable_to_non_nullable as Map,paragraphProperties: null == paragraphProperties ? _self._paragraphProperties : paragraphProperties // ignore: cast_nullable_to_non_nullable as Map, )); diff --git a/api/lib/src/models/text.g.dart b/api/lib/src/models/text.g.dart index 380bb82b6bcd..7651b9762e0f 100644 --- a/api/lib/src/models/text.g.dart +++ b/api/lib/src/models/text.g.dart @@ -227,6 +227,12 @@ Map _$TextAreaToJson(_TextArea instance) => { }; _TextStyleSheet _$TextStyleSheetFromJson(Map json) => _TextStyleSheet( + fontFamily: json['fontFamily'] as String? ?? 'Roboto', + fontFamilyFallback: + (json['fontFamilyFallback'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], spanProperties: (json['spanProperties'] as Map?)?.map( (k, e) => MapEntry( @@ -249,6 +255,8 @@ _TextStyleSheet _$TextStyleSheetFromJson(Map json) => _TextStyleSheet( Map _$TextStyleSheetToJson(_TextStyleSheet instance) => { + 'fontFamily': instance.fontFamily, + 'fontFamilyFallback': instance.fontFamilyFallback, 'spanProperties': instance.spanProperties.map( (k, e) => MapEntry(k, e.toJson()), ), diff --git a/api/test/data_test.dart b/api/test/data_test.dart index 6ece04f6b9de..68d6e7be6670 100644 --- a/api/test/data_test.dart +++ b/api/test/data_test.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:archive/archive.dart'; import 'package:butterfly_api/butterfly_api.dart'; +import 'package:butterfly_api/butterfly_text.dart'; import 'package:dart_leap/dart_leap.dart'; import 'package:test/test.dart'; @@ -11,6 +12,43 @@ DocumentPage _pageWithLayer(String layerId) => DocumentPage(layers: [DocumentLayer(id: layerId)]); void main() { + group('Pack fonts', () { + test('font assets can be listed, inherited, and removed', () { + final bytes = Uint8List.fromList([1, 2, 3]); + final parent = NoteData(Archive()).setFont('Example.ttf', bytes); + var pack = NoteData(Archive(), parent: parent); + + expect(pack.getFonts(), contains('Example.ttf')); + expect(pack.getFont('Example.ttf'), bytes); + + pack = pack.setFont('Local.otf', bytes); + expect(pack.getFonts(), containsAll(['Example.ttf', 'Local.otf'])); + pack = pack.removeFont('Local.otf'); + expect(pack.getFonts(), isNot(contains('Local.otf'))); + }); + }); + + group('Text style fonts', () { + test('old styles default to Roboto without fallbacks', () { + final style = TextStyleSheet.fromJson(const {}); + + expect(style.fontFamily, 'Roboto'); + expect(style.fontFamilyFallback, isEmpty); + }); + + test('font families round-trip through JSON', () { + const style = TextStyleSheet( + fontFamily: 'Custom-Example.ttf', + fontFamilyFallback: ['Noto Sans', 'Roboto'], + ); + + final decoded = TextStyleSheet.fromJson(style.toJson()); + + expect(decoded.fontFamily, style.fontFamily); + expect(decoded.fontFamilyFallback, style.fontFamilyFallback); + }); + }); + group('Highlighter options', () { test('pen tool options round-trip through JSON', () { final tool = PenTool(id: 'highlighter', combinePaths: true); diff --git a/app/lib/api/open.dart b/app/lib/api/open.dart index aa092d0e9969..1d5fa2c97f4e 100644 --- a/app/lib/api/open.dart +++ b/app/lib/api/open.dart @@ -122,6 +122,21 @@ Future> importFiles( return files; } +Future> importFilesWithExtensions( + List extensions, +) async { + final result = await FilePicker.pickFiles( + allowedExtensions: extensions, + type: FileType.custom, + ); + if (result == null) return []; + final files = <(Uint8List, String, String)>[]; + for (final file in result.files) { + files.add(await _readPlatformFile(file)); + } + return files; +} + Future openFile( BuildContext context, bool replace, diff --git a/app/lib/dialogs/packs/fonts.dart b/app/lib/dialogs/packs/fonts.dart new file mode 100644 index 000000000000..b63cdb58ec46 --- /dev/null +++ b/app/lib/dialogs/packs/fonts.dart @@ -0,0 +1,91 @@ +import 'package:butterfly/api/open.dart'; +import 'package:butterfly/services/font.dart'; +import 'package:butterfly/src/generated/i18n/app_localizations.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +class FontsPackView extends StatelessWidget { + final NoteData value; + final ValueChanged onChanged; + + const FontsPackView({ + super.key, + required this.value, + required this.onChanged, + }); + + Future _importFonts(BuildContext context) async { + final files = await importFilesWithExtensions(const ['ttf', 'otf']); + if (!context.mounted || files.isEmpty) return; + var pack = value; + var imported = false; + var failed = false; + for (final (data, extension, name) in files) { + final fileName = pack.findUniqueName( + kFontsArchiveDirectory, + extension, + name, + ); + final family = await context.read().loadFont( + fileName, + ByteData.sublistView(data), + ); + if (family == null) { + failed = true; + continue; + } + pack = pack.setFont(fileName, data); + imported = true; + } + if (failed && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context).errorWhileImporting), + ), + ); + } + if (imported) onChanged(pack); + } + + @override + Widget build(BuildContext context) { + final fonts = value.getFonts().toList()..sort(); + return Stack( + children: [ + ListView( + padding: const EdgeInsets.only(bottom: 80), + children: fonts + .map( + (name) => ListTile( + leading: const PhosphorIcon(PhosphorIconsLight.textAa), + title: Text(name), + subtitle: Text(customFontFamily(_baseName(name))), + trailing: IconButton( + icon: const PhosphorIcon(PhosphorIconsLight.trash), + tooltip: AppLocalizations.of(context).delete, + onPressed: () => onChanged(value.removeFont(name)), + ), + ), + ) + .toList(), + ), + Align( + alignment: Alignment.bottomCenter, + child: FloatingActionButton.extended( + onPressed: () => _importFonts(context), + icon: const PhosphorIcon(PhosphorIconsLight.uploadSimple), + label: Text(AppLocalizations.of(context).import), + ), + ), + ], + ); + } + + String _baseName(String name) { + final dot = name.lastIndexOf('.'); + return dot > 0 ? name.substring(0, dot) : name; + } +} diff --git a/app/lib/dialogs/packs/pack.dart b/app/lib/dialogs/packs/pack.dart index 4b70439a27e9..99cc6e0873de 100644 --- a/app/lib/dialogs/packs/pack.dart +++ b/app/lib/dialogs/packs/pack.dart @@ -7,6 +7,7 @@ import 'package:material_leap/material_leap.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import 'components.dart'; +import 'fonts.dart'; import 'general.dart'; import 'palettes.dart'; import 'styles/view.dart'; @@ -50,7 +51,7 @@ class _PackDialogState extends State { ), constraints: const BoxConstraints(maxWidth: 700, maxHeight: 800), content: DefaultTabController( - length: widget.pack == null ? 1 : 7, + length: widget.pack == null ? 1 : 8, child: Column( children: [ if (widget.pack != null) @@ -75,6 +76,7 @@ class _PackDialogState extends State { AppLocalizations.of(context).palettes, ), (PhosphorIconsLight.imageSquare, 'Textures'), + (PhosphorIconsLight.textAa, 'Fonts'), ( PhosphorIconsLight.toolbox, AppLocalizations.of(context).toolbars, @@ -102,6 +104,7 @@ class _PackDialogState extends State { StylesPackView(value: pack, onChanged: _onChanged), PalettesPackView(value: pack, onChanged: _onChanged), TexturesPackView(value: pack, onChanged: _onChanged), + FontsPackView(value: pack, onChanged: _onChanged), ToolbarsPackView(value: pack, onChanged: _onChanged), ToolPresetsPackView(value: pack, onChanged: _onChanged), ], diff --git a/app/lib/dialogs/packs/styles/general.dart b/app/lib/dialogs/packs/styles/general.dart index d89531387699..465a46b59460 100644 --- a/app/lib/dialogs/packs/styles/general.dart +++ b/app/lib/dialogs/packs/styles/general.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:material_leap/material_leap.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; +import '../../../widgets/font_style_field.dart'; + class GeneralStyleView extends StatelessWidget { final TextStyleSheet value; final ValueChanged onChanged; @@ -31,6 +33,21 @@ class GeneralStyleView extends StatelessWidget { initialValue: name, onChanged: onNameChanged, ), + const SizedBox(height: 16), + FontStyleField( + fontFamily: value.fontFamily, + fontFamilyFallback: value.fontFamilyFallback, + onFontFamilyChanged: (fontFamily) => onChanged( + value.copyWith( + fontFamily: fontFamily, + fontFamilyFallback: value.fontFamilyFallback + .where((fallback) => fallback != fontFamily) + .toList(), + ), + ), + onFontFamilyFallbackChanged: (fontFamilyFallback) => + onChanged(value.copyWith(fontFamilyFallback: fontFamilyFallback)), + ), ], ); } diff --git a/app/lib/main.dart b/app/lib/main.dart index a9c672109ec6..94fddeaaf0d9 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -34,6 +34,7 @@ import 'views/error.dart'; import 'views/home/page.dart'; import 'views/main.dart'; import 'services/logger.dart'; +import 'services/font.dart'; const platform = MethodChannel('linwood.dev/butterfly'); @@ -370,10 +371,14 @@ class ButterflyApp extends StatelessWidget { dispose: (fileSystem) => fileSystem.dispose(), child: RepositoryProvider( create: (context) => - SyncService(context, context.read()), - dispose: (service) => service.dispose(), - lazy: false, - child: _WindowCloseGuard(child: child ?? Container()), + FontService(context.read()), + child: RepositoryProvider( + create: (context) => + SyncService(context, context.read()), + dispose: (service) => service.dispose(), + lazy: false, + child: _WindowCloseGuard(child: child ?? Container()), + ), ), ); }, diff --git a/app/lib/renderers/elements/text.dart b/app/lib/renderers/elements/text.dart index fa9323277ac2..9321083c68ac 100644 --- a/app/lib/renderers/elements/text.dart +++ b/app/lib/renderers/elements/text.dart @@ -38,7 +38,12 @@ abstract class GenericTextRenderer extends Renderer { (i, e) => _createSpan(document, dimensions, i, e, style), ) .toList(), - style: style.span.toFlutter(null, element.foreground), + style: style.span.toFlutter( + null, + element.foreground, + styleSheet?.fontFamily ?? kDefaultFontFamily, + styleSheet?.fontFamilyFallback ?? const [], + ), ), }; _tp?.setPlaceholderDimensions(dimensions); @@ -58,7 +63,12 @@ abstract class GenericTextRenderer extends Renderer { final styleSheet = _getStyle(); final style = styleSheet .resolveSpanProperty(span.property) - ?.toFlutter(parent, element.foreground); + ?.toFlutter( + parent, + element.foreground, + styleSheet?.fontFamily ?? kDefaultFontFamily, + styleSheet?.fontFamilyFallback ?? const [], + ); switch (span) { case text.TextSpan(): return TextSpan(text: span.text, style: style); @@ -133,7 +143,12 @@ abstract class GenericTextRenderer extends Renderer { textStyle: (styleSheet.resolveSpanProperty(span.property) ?? const text.DefinedSpanProperty()) - .toFlutter(paragraphStyle, element.foreground), + .toFlutter( + paragraphStyle, + element.foreground, + styleSheet?.fontFamily ?? kDefaultFontFamily, + styleSheet?.fontFamilyFallback ?? const [], + ), ), ); @@ -278,7 +293,14 @@ abstract class GenericTextRenderer extends Renderer { for (final span in paragraph.textSpans) { final style = styles.resolveSpanProperty(span.property); textElement.createElement('tspan') - ..setAttribute('style', style?.toCss()) + ..setAttribute( + 'style', + style?.toCss( + null, + styles?.fontFamily ?? kDefaultFontFamily, + styles?.fontFamilyFallback ?? const [], + ), + ) ..innerText = _convertTextToHtml(span.text); } } diff --git a/app/lib/renderers/renderer.dart b/app/lib/renderers/renderer.dart index f906f90f51bc..3cbfdc9893d8 100644 --- a/app/lib/renderers/renderer.dart +++ b/app/lib/renderers/renderer.dart @@ -34,6 +34,7 @@ import '../cubits/transform.dart'; import '../helpers/xml.dart'; import '../models/label.dart'; import '../services/asset.dart'; +import '../services/font.dart'; import '../services/logger.dart'; import 'textures/texture.dart'; diff --git a/app/lib/services/font.dart b/app/lib/services/font.dart new file mode 100644 index 000000000000..44cbf00c94cb --- /dev/null +++ b/app/lib/services/font.dart @@ -0,0 +1,106 @@ +import 'package:butterfly/api/file_system.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:flutter/services.dart'; +import 'package:lw_sysapi/lw_sysapi.dart'; + +const kCustomFontPrefix = 'Custom-'; +const kDefaultFontFamily = 'Roboto'; +const kBundledFontFamilies = { + 'Comfortaa', + 'Noto Sans Arabic', + kDefaultFontFamily, +}; + +String customFontFamily(String name) => '$kCustomFontPrefix$name'; + +class AvailableFontFamily { + final String name; + final bool bundled; + final bool system; + + const AvailableFontFamily({ + required this.name, + required this.bundled, + required this.system, + }); +} + +class FontService { + final ButterflyFileSystem _fileSystem; + Future>? _systemFonts; + final Map> _loadedFonts = {}; + final Set _customFonts = {}; + + FontService(this._fileSystem); + + Future> getSystemFonts() => _systemFonts ??= _fetchSystemFonts(); + + Future> getFonts() async { + await loadFonts(); + final systemFonts = (await getSystemFonts()).toSet(); + final bundledFonts = {...kBundledFontFamilies, ..._customFonts}; + final names = {...systemFonts, ...bundledFonts}.toList()..sort(); + return names + .map( + (name) => AvailableFontFamily( + name: name, + bundled: bundledFonts.contains(name), + system: systemFonts.contains(name), + ), + ) + .toList(); + } + + Future loadFonts([NoteData? document]) async { + try { + final packs = await _fileSystem.getCoreAndUserPacks(); + for (final (_, pack) in packs) { + await loadFontsFromPack(pack); + } + if (document != null) { + for (final name in document.getBundledPacks()) { + final pack = document.getBundledPack(name); + if (pack != null) await loadFontsFromPack(pack); + } + } + } catch (_) { + // A remote or unavailable pack store must not hide bundled/system fonts. + } + } + + Future loadFontsFromPack(NoteData pack) async { + for (final name in pack.getFonts()) { + final data = pack.getFont(name); + if (data != null) await loadFont(name, ByteData.sublistView(data)); + } + } + + Future> _fetchSystemFonts() async { + try { + final fonts = await SysAPI.getFonts() ?? const []; + return fonts.toSet().toList()..sort(); + } catch (_) { + return const []; + } + } + + Future loadFont(String name, ByteData data) async { + final dot = name.lastIndexOf('.'); + final baseName = dot > 0 ? name.substring(0, dot) : name; + final family = baseName.startsWith(kCustomFontPrefix) + ? baseName + : customFontFamily(baseName); + final loading = _loadedFonts.putIfAbsent(family, () async { + final loader = FontLoader(family)..addFont(Future.value(data)); + await loader.load(); + }); + try { + await loading; + _customFonts.add(family); + return family; + } catch (_) { + _loadedFonts.remove(family); + return null; + } + } +} diff --git a/app/lib/setup.dart b/app/lib/setup.dart index 74c098c192b9..be0a8e54dd4e 100644 --- a/app/lib/setup.dart +++ b/app/lib/setup.dart @@ -37,7 +37,7 @@ void setupLicenses() { 'Roboto', ], await rootBundle.loadString('fonts/Roboto-LICENSE.txt')); yield LicenseEntryWithLineBreaks([ - 'NotoSansArabic', + 'Noto Sans Arabic', ], await rootBundle.loadString('fonts/NotoSansArabic-LICENSE.txt')); }); } diff --git a/app/lib/theme.dart b/app/lib/theme.dart index b649eae99924..3595a72eb9df 100644 --- a/app/lib/theme.dart +++ b/app/lib/theme.dart @@ -49,7 +49,7 @@ ThemeData getThemeData( fontFamily: 'Comfortaa', visualDensity: density, darkIsTrueBlack: highContrast, - fontFamilyFallback: ['NotoSansArabic', 'Roboto'], + fontFamilyFallback: ['Noto Sans Arabic', 'Roboto'], ); } else { theme = FlexThemeData.light( @@ -60,7 +60,7 @@ ThemeData getThemeData( fontFamily: 'Comfortaa', visualDensity: density, lightIsWhite: highContrast, - fontFamilyFallback: ['NotoSansArabic', 'Roboto'], + fontFamilyFallback: ['Noto Sans Arabic', 'Roboto'], ); } return theme.copyWith( diff --git a/app/lib/views/main.dart b/app/lib/views/main.dart index ac061d518a49..4b511b8c77b9 100644 --- a/app/lib/views/main.dart +++ b/app/lib/views/main.dart @@ -15,6 +15,7 @@ import 'package:butterfly/models/persisted_document_state.dart'; import 'package:butterfly/renderers/renderer.dart'; import 'package:butterfly/repositories/document_state.dart'; import 'package:butterfly/services/export.dart'; +import 'package:butterfly/services/font.dart'; import 'package:butterfly/services/import.dart'; import 'package:butterfly/services/network.dart'; import 'package:butterfly/views/app_bar.dart'; @@ -333,6 +334,11 @@ class _ProjectPageState extends State { createdAt: DateTime.now(), ); } + await context.read().loadFonts(document); + if (!isCurrentLoad()) { + await disposePendingRuntime(); + return; + } location ??= AssetLocation( path: widget.location?.path ?? '', remote: remote?.identifier ?? '', diff --git a/app/lib/views/toolbar/label.dart b/app/lib/views/toolbar/label.dart index 575440fc0efe..17f885af208a 100644 --- a/app/lib/views/toolbar/label.dart +++ b/app/lib/views/toolbar/label.dart @@ -15,6 +15,7 @@ import '../../bloc/document_bloc.dart'; import '../../dialogs/packs/select.dart'; import '../../models/defaults.dart'; import '../../models/label.dart'; +import '../../widgets/font_style_field.dart'; import 'view.dart'; class LabelToolbarView extends StatefulWidget implements PreferredSizeWidget { @@ -107,6 +108,25 @@ class _LabelToolbarViewState extends State { }; final named = value.getNamedStyleSheet(document); final styleSheet = named?.item; + void updateStyleSheet(text.TextStyleSheet style) { + final updated = NamedItem( + name: named?.name ?? '', + item: style, + ); + var newValue = value.copyWith( + tool: value.tool.copyWith(styleSheet: updated), + ); + newValue = switch (value) { + TextContext e => e.copyWith( + element: e.element?.copyWith(styleSheet: updated), + ), + MarkdownContext e => e.copyWith( + element: e.element?.copyWith(styleSheet: updated), + ), + }; + widget.onChanged(newValue); + } + _scaleController.text = (value.labelElement?.scale ?? value.tool.scale) .toString(); _sizeController.text = property?.getSize(paragraph).toString() ?? ''; @@ -579,32 +599,32 @@ class _LabelToolbarViewState extends State { ), ), const SizedBox(width: 16), - /*FutureBuilder?>( - future: Future.value(SysInfo.getFonts()), - builder: (context, snapshot) { - return DropdownMenu( - dropdownMenuEntries: snapshot.data - ?.map((e) => DropdownMenuEntry( - value: e, - label: e, - )) - .toList() ?? - [], - enableFilter: true, - onSelected: (value) { - if (kDebugMode) { - print(value); - } - }, - width: 200, - label: - Text(AppLocalizations.of(context).fontFamily), - inputDecorationTheme: - const InputDecorationTheme(filled: true), - ); - }, + FontStyleField( + fontFamily: styleSheet?.fontFamily ?? 'Roboto', + fontFamilyFallback: + styleSheet?.fontFamilyFallback ?? const [], + onFontFamilyChanged: (fontFamily) => updateStyleSheet( + (styleSheet ?? const text.TextStyleSheet()) + .copyWith( + fontFamily: fontFamily, + fontFamilyFallback: + styleSheet?.fontFamilyFallback + .where( + (fallback) => + fallback != fontFamily, + ) + .toList() ?? + const [], + ), + ), + onFontFamilyFallbackChanged: (fallback) => + updateStyleSheet( + (styleSheet ?? const text.TextStyleSheet()) + .copyWith(fontFamilyFallback: fallback), + ), + compact: true, ), - const SizedBox(width: 8),*/ + const SizedBox(width: 8), SizedBox( width: 100, child: TextFormField( diff --git a/app/lib/visualizer/text.dart b/app/lib/visualizer/text.dart index cd53e8eabfdb..7fbc84c6757b 100644 --- a/app/lib/visualizer/text.dart +++ b/app/lib/visualizer/text.dart @@ -3,6 +3,13 @@ import 'package:flutter/material.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; import 'package:material_leap/material_leap.dart'; +import '../services/font.dart'; + +String _fontFamilyToCss(String family) => "'${family.replaceAll("'", "\\'")}'"; + +String _fontFamiliesToCss(String family, List fallback) => + [family, ...fallback].map(_fontFamilyToCss).join(', '); + extension HorizontalTextAlignmentFlutterConverter on text.HorizontalAlignment { TextAlign toFlutter() => switch (this) { text.HorizontalAlignment.left => TextAlign.left, @@ -28,11 +35,14 @@ extension DefinedSpanPropertyFlutterConverter on text.DefinedSpanProperty { TextStyle toFlutter([ text.DefinedParagraphProperty? parent, SRGBColor? foreground, + String fontFamily = kDefaultFontFamily, + List fontFamilyFallback = const [], ]) { return TextStyle( fontSize: getSize(parent), color: getColor(parent, foreground).toColor(), - fontFamily: 'Roboto', + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, fontStyle: getItalic(parent) ? FontStyle.italic : FontStyle.normal, fontWeight: FontWeight.values[getFontWeight(parent)], letterSpacing: getLetterSpacing(parent), @@ -48,8 +58,13 @@ extension DefinedSpanPropertyFlutterConverter on text.DefinedSpanProperty { ); } - String toCss([text.DefinedParagraphProperty? parent]) => + String toCss([ + text.DefinedParagraphProperty? parent, + String fontFamily = kDefaultFontFamily, + List fontFamilyFallback = const [], + ]) => """ + font-family: ${_fontFamiliesToCss(fontFamily, fontFamilyFallback)}; font-weight: ${getFontWeight(parent)}; font-style: ${getItalic(parent) ? 'italic' : 'normal'}; font-size: ${getSize(parent)}px; @@ -64,9 +79,12 @@ extension DefinedSpanPropertyFlutterConverter on text.DefinedSpanProperty { } extension DefinedParagraphPropertyVisualizer on text.DefinedParagraphProperty { - String toCss() => + String toCss([ + String fontFamily = kDefaultFontFamily, + List fontFamilyFallback = const [], + ]) => """ -${span.toCss(this)} +${span.toCss(this, fontFamily, fontFamilyFallback)} text-align: ${alignment.toFlutter().toString().split('.').last}; """; } @@ -80,10 +98,12 @@ extension StyleSheetVisualizer on text.TextStyleSheet { String spanPrefix = kStyleSpanPrefix, }) => [ ...paragraphProperties.entries.map( - (e) => '$paragraphPrefix${e.key}{\n${e.value.toCss()}}', + (e) => + '$paragraphPrefix${e.key}{\n${e.value.toCss(fontFamily, fontFamilyFallback)}}', ), ...spanProperties.entries.map( - (e) => '$spanPrefix${e.key}{\n${e.value.toCss()}}', + (e) => + '$spanPrefix${e.key}{\n${e.value.toCss(null, fontFamily, fontFamilyFallback)}}', ), ].join('\n'); } diff --git a/app/lib/widgets/font_style_field.dart b/app/lib/widgets/font_style_field.dart new file mode 100644 index 000000000000..7ff232a51a7f --- /dev/null +++ b/app/lib/widgets/font_style_field.dart @@ -0,0 +1,157 @@ +import 'package:butterfly/services/font.dart'; +import 'package:butterfly/src/generated/i18n/app_localizations.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; + +class FontStyleField extends StatelessWidget { + final String fontFamily; + final List fontFamilyFallback; + final ValueChanged onFontFamilyChanged; + final ValueChanged> onFontFamilyFallbackChanged; + final bool compact; + + const FontStyleField({ + super.key, + required this.fontFamily, + required this.fontFamilyFallback, + required this.onFontFamilyChanged, + required this.onFontFamilyFallbackChanged, + this.compact = false, + }); + + List _availableFonts( + List loadedFonts, + ) { + final fonts = {for (final font in loadedFonts) font.name: font}; + for (final name in [fontFamily, ...fontFamilyFallback]) { + fonts.putIfAbsent( + name, + () => AvailableFontFamily(name: name, bundled: false, system: false), + ); + } + return fonts.values.toList()..sort((a, b) => a.name.compareTo(b.name)); + } + + void _selectFont(String? font) { + if (font == null) return; + onFontFamilyChanged(font); + } + + Widget? _sourceIcon(AvailableFontFamily font) { + final icon = switch ((font.bundled, font.system)) { + (true, true) => PhosphorIconsLight.intersect, + (true, false) => PhosphorIconsLight.package, + (false, true) => PhosphorIconsLight.monitor, + (false, false) => null, + }; + return icon == null ? null : PhosphorIcon(icon); + } + + Widget _buildFontMenu( + BuildContext context, + List fonts, + ) => DropdownMenu( + key: ValueKey(('font', fontFamily, fontFamilyFallback, fonts)), + width: compact ? 260 : null, + initialSelection: fontFamily, + enableFilter: true, + requestFocusOnTap: true, + expandedInsets: compact ? null : EdgeInsets.zero, + label: Text(AppLocalizations.of(context).fontFamily), + leadingIcon: const PhosphorIcon(PhosphorIconsLight.textAa), + dropdownMenuEntries: fonts.map((availableFont) { + final font = availableFont.name; + final fallbackIndex = fontFamilyFallback.indexOf(font); + final isPrimary = font == fontFamily; + return DropdownMenuEntry( + value: font, + leadingIcon: _sourceIcon(availableFont), + label: isPrimary + ? [fontFamily, ...fontFamilyFallback].join(', ') + : fallbackIndex < 0 + ? font + : '$font (fallback ${fallbackIndex + 1})', + trailingIcon: fallbackIndex < 0 + ? null + : const PhosphorIcon(PhosphorIconsLight.arrowRight), + ); + }).toList(), + onSelected: _selectFont, + ); + + void _toggleFallback(String font, bool selected) { + final fallback = fontFamilyFallback.where((item) => item != font).toList(); + if (selected) fallback.add(font); + onFontFamilyFallbackChanged(fallback); + } + + Widget _buildFallbackMenu(List fonts) => MenuAnchor( + menuChildren: fonts + .map( + (availableFont) => CheckboxMenuButton( + value: fontFamilyFallback.contains(availableFont.name), + onChanged: availableFont.name == fontFamily + ? null + : (selected) => + _toggleFallback(availableFont.name, selected ?? false), + child: Text(availableFont.name), + ), + ) + .toList(), + builder: (context, controller, child) { + void toggle() => + controller.isOpen ? controller.close() : controller.open(); + final icon = Badge( + isLabelVisible: fontFamilyFallback.isNotEmpty, + label: Text('${fontFamilyFallback.length}'), + child: const PhosphorIcon(PhosphorIconsLight.listPlus), + ); + if (compact) { + return IconButton( + onPressed: toggle, + tooltip: 'Fallback fonts', + icon: icon, + ); + } + final label = fontFamilyFallback.isEmpty + ? 'Fallback fonts' + : 'Fallback fonts: ${fontFamilyFallback.join(', ')}'; + return Align( + alignment: Alignment.centerLeft, + child: OutlinedButton.icon( + onPressed: toggle, + icon: icon, + label: Text(label), + ), + ); + }, + ); + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: context.read().getFonts(), + builder: (context, snapshot) { + final fonts = _availableFonts(snapshot.data ?? const []); + if (compact) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildFontMenu(context, fonts), + _buildFallbackMenu(fonts), + ], + ); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildFontMenu(context, fonts), + const SizedBox(height: 8), + _buildFallbackMenu(fonts), + ], + ); + }, + ); + } +} diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 182daeecd331..f6f967eee13a 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -157,7 +157,7 @@ flutter: # the material Icons class. uses-material-design: true fonts: - - family: NotoSansArabic + - family: Noto Sans Arabic fonts: - asset: fonts/NotoSansArabic-Light.ttf weight: 300 diff --git a/app/test/views/project_page_lifecycle_test.dart b/app/test/views/project_page_lifecycle_test.dart index e9f3b4e0e9b5..1bdfa08818db 100644 --- a/app/test/views/project_page_lifecycle_test.dart +++ b/app/test/views/project_page_lifecycle_test.dart @@ -4,6 +4,7 @@ import 'package:butterfly/bloc/document_bloc.dart'; import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/models/defaults.dart'; +import 'package:butterfly/services/font.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; import 'package:butterfly/views/main.dart'; import 'package:butterfly_api/butterfly_api.dart'; @@ -143,6 +144,9 @@ void main() { return MultiRepositoryProvider( providers: [ RepositoryProvider.value(value: fileSystem), + RepositoryProvider( + create: (context) => FontService(fileSystem), + ), RepositoryProvider.value( value: UnsupportedClipboardManager(), ), From 1fca044bd5571462b8cfe3f4c59e238a92da83cd Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 14 Jul 2026 22:07:18 +0200 Subject: [PATCH 086/117] Add update flathub beta workflow --- .github/workflows/build.yml | 208 ++++++++++++++++++++++++++++++++++++ app/pubspec.lock | 24 ++--- 2 files changed, 220 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8bb3a227d4d7..0cc3450acb58 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -940,3 +940,211 @@ jobs: version: ${{ needs.deploy.outputs.version }} release-tag: v${{ needs.deploy.outputs.version }} token: ${{ secrets.CI_PAT }} + update-flathub-beta: + name: Update Flathub beta + needs: + - deploy + if: >- + ${{ + github.event_name == 'release' && + github.event.action == 'published' && + github.event.release.prerelease == true + }} + runs-on: ubuntu-24.04 + + permissions: + contents: read + + concurrency: + group: flathub-beta-${{ github.event.release.tag_name }} + cancel-in-progress: false + + env: + FLATHUB_REPOSITORY: flathub/dev.linwood.butterfly + FLATHUB_MANIFEST: dev.linwood.butterfly.json + RELEASE_TAG: ${{ github.event.release.tag_name }} + + steps: + - name: Download release checksums + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p release + + for attempt in {1..12}; do + rm -f release/checksums.txt + + if gh release download "$RELEASE_TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --pattern checksums.txt \ + --dir release; then + break + fi + + echo "Release assets are not available yet, retrying..." + sleep 10 + done + + test -s release/checksums.txt + + - name: Read Linux checksums + id: checksums + run: | + X86_64_SHA="$( + awk '$2 == "linwood-butterfly-linux-x86_64.tar.gz" { + print $1 + }' release/checksums.txt + )" + + ARM64_SHA="$( + awk '$2 == "linwood-butterfly-linux-arm64.tar.gz" { + print $1 + }' release/checksums.txt + )" + + if [[ ! "$X86_64_SHA" =~ ^[0-9a-f]{64}$ ]]; then + echo "Invalid or missing x86_64 checksum" + exit 1 + fi + + if [[ ! "$ARM64_SHA" =~ ^[0-9a-f]{64}$ ]]; then + echo "Invalid or missing arm64 checksum" + exit 1 + fi + + echo "x86_64=$X86_64_SHA" >> "$GITHUB_OUTPUT" + echo "arm64=$ARM64_SHA" >> "$GITHUB_OUTPUT" + + - name: Checkout Flathub beta branch + uses: actions/checkout@v6 + with: + repository: flathub/dev.linwood.butterfly + ref: beta + token: ${{ secrets.CI_PAT }} + fetch-depth: 0 + path: flathub + + - name: Update Flathub manifest + env: + X86_64_SHA: ${{ steps.checksums.outputs.x86_64 }} + ARM64_SHA: ${{ steps.checksums.outputs.arm64 }} + run: | + python3 <<'PY' + import os + import re + from pathlib import Path + + manifest = Path("flathub") / os.environ["FLATHUB_MANIFEST"] + release_tag = os.environ["RELEASE_TAG"] + + files = { + "linwood-butterfly-linux-x86_64.tar.gz": os.environ["X86_64_SHA"], + "linwood-butterfly-linux-arm64.tar.gz": os.environ["ARM64_SHA"], + } + + lines = manifest.read_text(encoding="utf-8").splitlines() + updated = set() + + for index, line in enumerate(lines): + for filename, checksum in files.items(): + if '"url":' not in line or filename not in line: + continue + + indent = line[: len(line) - len(line.lstrip())] + lines[index] = ( + f'{indent}"url": ' + f'"https://github.com/LinwoodDev/Butterfly/' + f'releases/download/{release_tag}/{filename}",' + ) + + for checksum_index in range( + index + 1, + min(index + 6, len(lines)), + ): + if '"sha256":' not in lines[checksum_index]: + continue + + lines[checksum_index] = re.sub( + r'"sha256":\s*"[^"]+"', + f'"sha256": "{checksum}"', + lines[checksum_index], + ) + updated.add(filename) + break + + missing = set(files) - updated + if missing: + raise RuntimeError( + f"Could not update manifest entries: {sorted(missing)}" + ) + + manifest.write_text( + "\n".join(lines) + "\n", + encoding="utf-8", + ) + PY + + - name: Commit and push update branch + id: push + working-directory: flathub + run: | + VERSION="${RELEASE_TAG#v}" + BRANCH="automation/butterfly-$VERSION" + + echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" + + if git diff --quiet -- "$FLATHUB_MANIFEST"; then + echo "Manifest already contains this release" + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + git config user.name "Linwood CI" + git config user.email "ci@linwood.dev" + + git checkout -B "$BRANCH" + git add "$FLATHUB_MANIFEST" + git commit -m "Update Butterfly to $VERSION" + git push --force origin "HEAD:refs/heads/$BRANCH" + + echo "changed=true" >> "$GITHUB_OUTPUT" + + - name: Create Flathub beta pull request + if: steps.push.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.CI_PAT }} + BRANCH: ${{ steps.push.outputs.branch }} + run: | + EXISTING_PR="$( + gh pr list \ + --repo "$FLATHUB_REPOSITORY" \ + --base beta \ + --head "$BRANCH" \ + --state open \ + --json number \ + --jq '.[0].number // empty' + )" + + if [[ -n "$EXISTING_PR" ]]; then + echo "Pull request #$EXISTING_PR already exists" + exit 0 + fi + + VERSION="${RELEASE_TAG#v}" + + cat > /tmp/flathub-pr.md < Date: Wed, 15 Jul 2026 09:06:56 +0200 Subject: [PATCH 087/117] Fix embed/web loading errors, closes #1167 --- app/lib/api/file_system.dart | 34 +++---- app/lib/repositories/document_state.dart | 88 ++++++++++++------- app/lib/views/main.dart | 7 +- .../views/project_page_lifecycle_test.dart | 68 ++++++++++++-- metadata/en-US/changelogs/188.txt | 1 + 5 files changed, 143 insertions(+), 55 deletions(-) diff --git a/app/lib/api/file_system.dart b/app/lib/api/file_system.dart index f963d1d27238..863ea9e2d22d 100644 --- a/app/lib/api/file_system.dart +++ b/app/lib/api/file_system.dart @@ -208,32 +208,38 @@ class ButterflyFileSystem { static const _database = 'butterfly.db'; static const _databaseVersion = 5; - static Future _upgradeDatabase(VersionChangeEvent event) async { final db = event.database; if (event.oldVersion < 1) { db.createObjectStore('documents'); } + if (event.oldVersion < 3) { + db.createObjectStore('templates'); + } + if (event.oldVersion < 4) { + db.createObjectStore('packs'); + db.createObjectStore('documents-data'); + } + if (event.oldVersion < 5) { + db.createObjectStore('documentstates'); + db.createObjectStore('documentstates-data'); + } if (event.oldVersion < 2) { - var txn = event.transaction; - var store = txn.objectStore('documents'); - var cursor = store.openCursor(); + final txn = event.transaction; + final store = txn.objectStore('documents'); + final cursor = store.openCursor(); + await Future.wait( await cursor.map>((cursor) async { - // Add type to each document - var doc = cursor.value as Map; + final doc = cursor.value as Map; doc['type'] = 'document'; await store.put(doc); }).toList(), ); } - if (event.oldVersion < 3) { - db.createObjectStore('templates'); - } + if (event.oldVersion < 4) { - db.createObjectStore('packs'); - db.createObjectStore('documents-data'); - var txn = event.transaction; + final txn = event.transaction; var store = txn.objectStore('templates'); var cursor = store.openCursor(); await Future.wait( @@ -261,10 +267,6 @@ class ButterflyFileSystem { ); await txn.completed; } - if (event.oldVersion < 5) { - db.createObjectStore('documentstates'); - db.createObjectStore('documentstates-data'); - } } String _cacheKey(ExternalStorage? storage) => storage?.identifier ?? 'local'; diff --git a/app/lib/repositories/document_state.dart b/app/lib/repositories/document_state.dart index 46c97d9d16b4..e7b6652b91ec 100644 --- a/app/lib/repositories/document_state.dart +++ b/app/lib/repositories/document_state.dart @@ -24,28 +24,36 @@ class DocumentStateRepository { String? contentHash, String? pathKey, bool allowContentHash = true, - }) => _lock.synchronized(() async { - final settings = _settings; - if (!settings.enabled) return null; - try { - await fileSystem.initialize(); - if (pathKey != null) { - final byPath = await _getFileOrNull(pathKey); - if (byPath != null) return _applySettings(byPath, settings); - } - if (allowContentHash && contentHash != null) { - final byContent = await _getFileOrNull( - documentStateContentKey(contentHash), - ); - if (byContent != null) return _applySettings(byContent, settings); - } - } on NetworkException catch (e, stackTrace) { - // Document state is optional. A cached remote document must still open - // when its separate state record is unavailable offline. - talker.warning('Failed to load document state', e, stackTrace); + }) { + final canLoadByPath = pathKey != null; + final canLoadByContent = allowContentHash && contentHash != null; + + if (!canLoadByPath && !canLoadByContent) { + return Future.value(null); } - return null; - }); + return _lock.synchronized(() async { + final settings = _settings; + if (!settings.enabled) return null; + try { + await fileSystem.initialize(); + if (pathKey != null) { + final byPath = await _getFileOrNull(pathKey); + if (byPath != null) return _applySettings(byPath, settings); + } + if (allowContentHash && contentHash != null) { + final byContent = await _getFileOrNull( + documentStateContentKey(contentHash), + ); + if (byContent != null) return _applySettings(byContent, settings); + } + } on NetworkException catch (e, stackTrace) { + // Document state is optional. A cached remote document must still open + // when its separate state record is unavailable offline. + talker.warning('Failed to load document state', e, stackTrace); + } + return null; + }); + } Future save( PersistedDocumentState state, { @@ -54,17 +62,33 @@ class DocumentStateRepository { String? previousContentKey, String? previousPathKey, required bool persistentChanged, - }) => _lock.synchronized(() async { - final settings = _settings; - if (!settings.enabled) return; - await fileSystem.initialize(); - await _updateFile(previousPathKey, pathKey, state, persistentChanged); - await _updateFile(previousContentKey, contentKey, state, persistentChanged); - _scheduleCleanupAfterSave( - contentHash: state.contentHash, - pathKey: state.pathKey, - ); - }); + }) { + final hasAnyKey = + contentKey != null || + pathKey != null || + previousContentKey != null || + previousPathKey != null; + + if (!hasAnyKey) { + return Future.value(); + } + return _lock.synchronized(() async { + final settings = _settings; + if (!settings.enabled) return; + await fileSystem.initialize(); + await _updateFile(previousPathKey, pathKey, state, persistentChanged); + await _updateFile( + previousContentKey, + contentKey, + state, + persistentChanged, + ); + _scheduleCleanupAfterSave( + contentHash: state.contentHash, + pathKey: state.pathKey, + ); + }); + } Future cleanup({String? contentHash, String? pathKey, DateTime? now}) => _lock.synchronized(() async { diff --git a/app/lib/views/main.dart b/app/lib/views/main.dart index 4b511b8c77b9..1925095a591d 100644 --- a/app/lib/views/main.dart +++ b/app/lib/views/main.dart @@ -343,8 +343,11 @@ class _ProjectPageState extends State { path: widget.location?.path ?? '', remote: remote?.identifier ?? '', ); - final pathKey = documentStatePathKeyOrNull(location); - final contentHash = loadedDocumentBytes == null + final persistDocumentState = embedding == null; + final pathKey = persistDocumentState + ? documentStatePathKeyOrNull(location) + : null; + final contentHash = !persistDocumentState || loadedDocumentBytes == null ? null : documentStateContentHash(loadedDocumentBytes); final documentStateRepository = DocumentStateRepository( diff --git a/app/test/views/project_page_lifecycle_test.dart b/app/test/views/project_page_lifecycle_test.dart index 1bdfa08818db..4057e21856de 100644 --- a/app/test/views/project_page_lifecycle_test.dart +++ b/app/test/views/project_page_lifecycle_test.dart @@ -3,7 +3,9 @@ import 'package:butterfly/api/open.dart'; import 'package:butterfly/bloc/document_bloc.dart'; import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; +import 'package:butterfly/embed/embedding.dart'; import 'package:butterfly/models/defaults.dart'; +import 'package:butterfly/models/persisted_document_state.dart'; import 'package:butterfly/services/font.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; import 'package:butterfly/views/main.dart'; @@ -95,11 +97,13 @@ void main() { ); } - Widget buildApp() { - final document = DocumentDefaults.createDocument( - name: 'Lifecycle test', - page: const DocumentPage(backgrounds: []), - ); + Widget buildApp({NoteData? embedDocument}) { + final document = + embedDocument ?? + DocumentDefaults.createDocument( + name: 'Lifecycle test', + page: const DocumentPage(backgrounds: []), + ); fileSystem.buildTemplateSystem().updateFile('default', document); router = GoRouter( initialLocation: '/', @@ -136,6 +140,13 @@ void main() { path: 'import', builder: (context, state) => ProjectPage(data: document), ), + GoRoute( + path: 'embed', + builder: (context, state) => ProjectPage( + data: document.toFile(), + embedding: Embedding(internal: true), + ), + ), ], ), ], @@ -222,6 +233,51 @@ void main() { 'imported document close', ); }); + + testWidgets('embed does not load or save persistent document state', ( + tester, + ) async { + final document = DocumentDefaults.createDocument( + name: 'Embed lifecycle test', + page: const DocumentPage(backgrounds: []), + ); + final contentKey = documentStateContentKey( + documentStateContentHash(document.exportAsBytes()), + ); + final documentStateSystem = fileSystem.buildDocumentStateSystem(); + await documentStateSystem.initialize(); + await documentStateSystem.createFile( + contentKey, + const PersistedDocumentState( + camera: PersistedCameraState(positionX: 100, positionY: 200, zoom: 3), + ), + ); + await tester.pumpWidget(buildApp(embedDocument: document)); + + router.go('/embed'); + await pumpUntil( + tester, + () => observer.lastDocumentBloc?.state is DocumentLoadSuccess, + 'embedded document open', + ); + + final editorController = observer.lastDocumentBloc!.editorController; + expect(editorController.transformCubit.state.position, Offset.zero); + expect(editorController.transformCubit.state.size, 1); + editorController.transformCubit.teleport(const Offset(10, 20), 2); + + router.go('/'); + await pumpUntil( + tester, + () => observer.documentBlocCloses == 1, + 'embedded document close', + ); + + final stored = await documentStateSystem.getFile(contentKey); + expect(stored?.camera.positionX, 100); + expect(stored?.camera.positionY, 200); + expect(stored?.camera.zoom, 3); + }); } class _LifecycleObserver extends BlocObserver { @@ -229,6 +285,7 @@ class _LifecycleObserver extends BlocObserver { int documentBlocCloses = 0; int saveCubitCreates = 0; int saveCubitCloses = 0; + DocumentBloc? lastDocumentBloc; DocumentSaveCubit? lastSaveCubit; final events = []; @@ -237,6 +294,7 @@ class _LifecycleObserver extends BlocObserver { super.onCreate(bloc); if (bloc is DocumentBloc) { documentBlocCreates++; + lastDocumentBloc = bloc; } else if (bloc is DocumentSaveCubit) { saveCubitCreates++; lastSaveCubit = bloc; diff --git a/metadata/en-US/changelogs/188.txt b/metadata/en-US/changelogs/188.txt index 961bb5dc7893..57e0a0e032a6 100644 --- a/metadata/en-US/changelogs/188.txt +++ b/metadata/en-US/changelogs/188.txt @@ -9,6 +9,7 @@ * Fix crash with android saf on folders with many files * Fix blur resetting on color change * Fix polygon collision aabb tests if closed ([#1162](https://github.com/LinwoodDev/Butterfly/pull/1162)) +* Fix embed/web loading errors ([#1167](https://github.com/LinwoodDev/Butterfly/issues/1167)) * Upgrade to agb 9 Read more here: https://linwood.dev/butterfly/2.6.0-beta.2 \ No newline at end of file From 29fdcb129d7395515482adb8422ef6812ca9bf5d Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Wed, 15 Jul 2026 11:42:39 +0200 Subject: [PATCH 088/117] Improve rotated elements transformtion, closes #1099 --- api/lib/src/models/element.dart | 9 + api/lib/src/models/element.freezed.dart | 139 +++++----- api/lib/src/models/element.g.dart | 18 ++ app/lib/l10n/app_en.arb | 3 +- app/lib/renderers/elements/image.dart | 2 + app/lib/renderers/elements/markdown.dart | 2 + app/lib/renderers/elements/pdf.dart | 2 + app/lib/renderers/elements/pen.dart | 38 ++- app/lib/renderers/elements/polygon.dart | 7 +- app/lib/renderers/elements/shape.dart | 12 +- app/lib/renderers/elements/svg.dart | 2 + app/lib/renderers/elements/text.dart | 2 + app/lib/renderers/elements/texture.dart | 2 + app/lib/renderers/renderer.dart | 279 ++++++++++++++------ app/lib/selections/elements/element.dart | 17 ++ app/lib/view_painter.dart | 19 +- app/test/renderers/shape_renderer_test.dart | 33 +++ metadata/en-US/changelogs/188.txt | 2 + 18 files changed, 404 insertions(+), 184 deletions(-) diff --git a/api/lib/src/models/element.dart b/api/lib/src/models/element.dart index 2185a1cf67c0..3d834adff9ec 100644 --- a/api/lib/src/models/element.dart +++ b/api/lib/src/models/element.dart @@ -85,6 +85,7 @@ sealed class PadElement with _$PadElement { @Implements() factory PadElement.pen({ @Default(0) double rotation, + @Default(0) double shear, @Default('') String collection, @IdJsonConverter() String? id, double? zoom, @@ -97,6 +98,7 @@ sealed class PadElement with _$PadElement { @With() factory PadElement.text({ @Default(0) double rotation, + @Default(0) double shear, @Default('') String collection, @IdJsonConverter() String? id, @DoublePointJsonConverter() @@ -113,6 +115,7 @@ sealed class PadElement with _$PadElement { @With() factory PadElement.markdown({ @Default(0) double rotation, + @Default(0) double shear, @Default('') String collection, @IdJsonConverter() String? id, @DoublePointJsonConverter() @@ -130,6 +133,7 @@ sealed class PadElement with _$PadElement { @Implements() factory PadElement.image({ @Default(0) double rotation, + @Default(0) double shear, @Default('') String collection, @IdJsonConverter() String? id, @DoublePointJsonConverter() @@ -146,6 +150,7 @@ sealed class PadElement with _$PadElement { @Implements() factory PadElement.pdf({ @Default(0) double rotation, + @Default(0) double shear, @Default('') String collection, @IdJsonConverter() String? id, @DoublePointJsonConverter() @@ -165,6 +170,7 @@ sealed class PadElement with _$PadElement { @Implements() factory PadElement.svg({ @Default(0) double rotation, + @Default(0) double shear, @Default('') String collection, @IdJsonConverter() String? id, @DoublePointJsonConverter() @@ -180,6 +186,7 @@ sealed class PadElement with _$PadElement { factory PadElement.shape({ @Default(0) double rotation, + @Default(0) double shear, @Default('') String collection, @IdJsonConverter() String? id, @DoublePointJsonConverter() @@ -194,6 +201,7 @@ sealed class PadElement with _$PadElement { factory PadElement.texture({ @Default(0) double rotation, + @Default(0) double shear, @Default('') String collection, @IdJsonConverter() String? id, @Default(SurfaceTexture.pattern()) SurfaceTexture texture, @@ -208,6 +216,7 @@ sealed class PadElement with _$PadElement { factory PadElement.polygon({ @Default(0) double rotation, + @Default(0) double shear, @Default('') String collection, @IdJsonConverter() String? id, @Default([]) List points, diff --git a/api/lib/src/models/element.freezed.dart b/api/lib/src/models/element.freezed.dart index 7ccc20bda3d9..6a8e99cfe8cb 100644 --- a/api/lib/src/models/element.freezed.dart +++ b/api/lib/src/models/element.freezed.dart @@ -496,7 +496,7 @@ PadElement _$PadElementFromJson( /// @nodoc mixin _$PadElement { - double get rotation; String get collection;@IdJsonConverter() String? get id; Map get extra; + double get rotation; double get shear; String get collection;@IdJsonConverter() String? get id; Map get extra; /// Create a copy of PadElement /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -509,16 +509,16 @@ $PadElementCopyWith get copyWith => _$PadElementCopyWithImpl Object.hash(runtimeType,rotation,collection,id,const DeepCollectionEquality().hash(extra)); +int get hashCode => Object.hash(runtimeType,rotation,shear,collection,id,const DeepCollectionEquality().hash(extra)); @override String toString() { - return 'PadElement(rotation: $rotation, collection: $collection, id: $id, extra: $extra)'; + return 'PadElement(rotation: $rotation, shear: $shear, collection: $collection, id: $id, extra: $extra)'; } @@ -529,7 +529,7 @@ abstract mixin class $PadElementCopyWith<$Res> { factory $PadElementCopyWith(PadElement value, $Res Function(PadElement) _then) = _$PadElementCopyWithImpl; @useResult $Res call({ - double rotation, String collection,@IdJsonConverter() String? id, Map extra + double rotation, double shear, String collection,@IdJsonConverter() String? id, Map extra }); @@ -546,9 +546,10 @@ class _$PadElementCopyWithImpl<$Res> /// Create a copy of PadElement /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? rotation = null,Object? collection = null,Object? id = freezed,Object? extra = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? rotation = null,Object? shear = null,Object? collection = null,Object? id = freezed,Object? extra = null,}) { return _then(_self.copyWith( rotation: null == rotation ? _self.rotation : rotation // ignore: cast_nullable_to_non_nullable +as double,shear: null == shear ? _self.shear : shear // ignore: cast_nullable_to_non_nullable as double,collection: null == collection ? _self.collection : collection // ignore: cast_nullable_to_non_nullable as String,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String?,extra: null == extra ? _self.extra : extra // ignore: cast_nullable_to_non_nullable @@ -564,10 +565,11 @@ as Map, @JsonSerializable() class PenElement extends PadElement implements PathElement { - PenElement({this.rotation = 0, this.collection = '', @IdJsonConverter() this.id, this.zoom, this.combineId, final List points = const [], this.property = const PenProperty(), final Map extra = const {}, final String? $type}): _points = points,_extra = extra,$type = $type ?? 'pen',super._(); + PenElement({this.rotation = 0, this.shear = 0, this.collection = '', @IdJsonConverter() this.id, this.zoom, this.combineId, final List points = const [], this.property = const PenProperty(), final Map extra = const {}, final String? $type}): _points = points,_extra = extra,$type = $type ?? 'pen',super._(); factory PenElement.fromJson(Map json) => _$PenElementFromJson(json); @override@JsonKey() final double rotation; +@override@JsonKey() final double shear; @override@JsonKey() final String collection; @override@IdJsonConverter() final String? id; final double? zoom; @@ -605,16 +607,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is PenElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&(identical(other.zoom, zoom) || other.zoom == zoom)&&(identical(other.combineId, combineId) || other.combineId == combineId)&&const DeepCollectionEquality().equals(other._points, _points)&&const DeepCollectionEquality().equals(other.property, property)&&const DeepCollectionEquality().equals(other._extra, _extra)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is PenElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.shear, shear) || other.shear == shear)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&(identical(other.zoom, zoom) || other.zoom == zoom)&&(identical(other.combineId, combineId) || other.combineId == combineId)&&const DeepCollectionEquality().equals(other._points, _points)&&const DeepCollectionEquality().equals(other.property, property)&&const DeepCollectionEquality().equals(other._extra, _extra)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,rotation,collection,id,zoom,combineId,const DeepCollectionEquality().hash(_points),const DeepCollectionEquality().hash(property),const DeepCollectionEquality().hash(_extra)); +int get hashCode => Object.hash(runtimeType,rotation,shear,collection,id,zoom,combineId,const DeepCollectionEquality().hash(_points),const DeepCollectionEquality().hash(property),const DeepCollectionEquality().hash(_extra)); @override String toString() { - return 'PadElement.pen(rotation: $rotation, collection: $collection, id: $id, zoom: $zoom, combineId: $combineId, points: $points, property: $property, extra: $extra)'; + return 'PadElement.pen(rotation: $rotation, shear: $shear, collection: $collection, id: $id, zoom: $zoom, combineId: $combineId, points: $points, property: $property, extra: $extra)'; } @@ -625,7 +627,7 @@ abstract mixin class $PenElementCopyWith<$Res> implements $PadElementCopyWith<$R factory $PenElementCopyWith(PenElement value, $Res Function(PenElement) _then) = _$PenElementCopyWithImpl; @override @useResult $Res call({ - double rotation, String collection,@IdJsonConverter() String? id, double? zoom, String? combineId, List points, PenProperty property, Map extra + double rotation, double shear, String collection,@IdJsonConverter() String? id, double? zoom, String? combineId, List points, PenProperty property, Map extra }); @@ -642,9 +644,10 @@ class _$PenElementCopyWithImpl<$Res> /// Create a copy of PadElement /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? collection = null,Object? id = freezed,Object? zoom = freezed,Object? combineId = freezed,Object? points = null,Object? property = freezed,Object? extra = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? shear = null,Object? collection = null,Object? id = freezed,Object? zoom = freezed,Object? combineId = freezed,Object? points = null,Object? property = freezed,Object? extra = null,}) { return _then(PenElement( rotation: null == rotation ? _self.rotation : rotation // ignore: cast_nullable_to_non_nullable +as double,shear: null == shear ? _self.shear : shear // ignore: cast_nullable_to_non_nullable as double,collection: null == collection ? _self.collection : collection // ignore: cast_nullable_to_non_nullable as String,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String?,zoom: freezed == zoom ? _self.zoom : zoom // ignore: cast_nullable_to_non_nullable @@ -663,10 +666,11 @@ as Map, @JsonSerializable() class TextElement extends PadElement with LabelElement { - TextElement({this.rotation = 0, this.collection = '', @IdJsonConverter() this.id, @DoublePointJsonConverter() this.position = const Point(0.0, 0.0), this.scale = 1.0, this.styleSheet, required this.area, this.constraint = const ElementConstraint(size: 1000), @ColorJsonConverter() this.foreground = SRGBColor.black, final Map extra = const {}, final String? $type}): _extra = extra,$type = $type ?? 'text',super._(); + TextElement({this.rotation = 0, this.shear = 0, this.collection = '', @IdJsonConverter() this.id, @DoublePointJsonConverter() this.position = const Point(0.0, 0.0), this.scale = 1.0, this.styleSheet, required this.area, this.constraint = const ElementConstraint(size: 1000), @ColorJsonConverter() this.foreground = SRGBColor.black, final Map extra = const {}, final String? $type}): _extra = extra,$type = $type ?? 'text',super._(); factory TextElement.fromJson(Map json) => _$TextElementFromJson(json); @override@JsonKey() final double rotation; +@override@JsonKey() final double shear; @override@JsonKey() final String collection; @override@IdJsonConverter() final String? id; @JsonKey()@DoublePointJsonConverter() final Point position; @@ -700,16 +704,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is TextElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&(identical(other.position, position) || other.position == position)&&(identical(other.scale, scale) || other.scale == scale)&&(identical(other.styleSheet, styleSheet) || other.styleSheet == styleSheet)&&(identical(other.area, area) || other.area == area)&&(identical(other.constraint, constraint) || other.constraint == constraint)&&(identical(other.foreground, foreground) || other.foreground == foreground)&&const DeepCollectionEquality().equals(other._extra, _extra)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is TextElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.shear, shear) || other.shear == shear)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&(identical(other.position, position) || other.position == position)&&(identical(other.scale, scale) || other.scale == scale)&&(identical(other.styleSheet, styleSheet) || other.styleSheet == styleSheet)&&(identical(other.area, area) || other.area == area)&&(identical(other.constraint, constraint) || other.constraint == constraint)&&(identical(other.foreground, foreground) || other.foreground == foreground)&&const DeepCollectionEquality().equals(other._extra, _extra)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,rotation,collection,id,position,scale,styleSheet,area,constraint,foreground,const DeepCollectionEquality().hash(_extra)); +int get hashCode => Object.hash(runtimeType,rotation,shear,collection,id,position,scale,styleSheet,area,constraint,foreground,const DeepCollectionEquality().hash(_extra)); @override String toString() { - return 'PadElement.text(rotation: $rotation, collection: $collection, id: $id, position: $position, scale: $scale, styleSheet: $styleSheet, area: $area, constraint: $constraint, foreground: $foreground, extra: $extra)'; + return 'PadElement.text(rotation: $rotation, shear: $shear, collection: $collection, id: $id, position: $position, scale: $scale, styleSheet: $styleSheet, area: $area, constraint: $constraint, foreground: $foreground, extra: $extra)'; } @@ -720,7 +724,7 @@ abstract mixin class $TextElementCopyWith<$Res> implements $PadElementCopyWith<$ factory $TextElementCopyWith(TextElement value, $Res Function(TextElement) _then) = _$TextElementCopyWithImpl; @override @useResult $Res call({ - double rotation, String collection,@IdJsonConverter() String? id,@DoublePointJsonConverter() Point position, double scale, NamedItem? styleSheet, TextArea area, ElementConstraint constraint,@ColorJsonConverter() SRGBColor foreground, Map extra + double rotation, double shear, String collection,@IdJsonConverter() String? id,@DoublePointJsonConverter() Point position, double scale, NamedItem? styleSheet, TextArea area, ElementConstraint constraint,@ColorJsonConverter() SRGBColor foreground, Map extra }); @@ -737,9 +741,10 @@ class _$TextElementCopyWithImpl<$Res> /// Create a copy of PadElement /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? collection = null,Object? id = freezed,Object? position = null,Object? scale = null,Object? styleSheet = freezed,Object? area = null,Object? constraint = null,Object? foreground = null,Object? extra = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? shear = null,Object? collection = null,Object? id = freezed,Object? position = null,Object? scale = null,Object? styleSheet = freezed,Object? area = null,Object? constraint = null,Object? foreground = null,Object? extra = null,}) { return _then(TextElement( rotation: null == rotation ? _self.rotation : rotation // ignore: cast_nullable_to_non_nullable +as double,shear: null == shear ? _self.shear : shear // ignore: cast_nullable_to_non_nullable as double,collection: null == collection ? _self.collection : collection // ignore: cast_nullable_to_non_nullable as String,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String?,position: null == position ? _self.position : position // ignore: cast_nullable_to_non_nullable @@ -790,10 +795,11 @@ $ElementConstraintCopyWith<$Res> get constraint { @JsonSerializable() class MarkdownElement extends PadElement with LabelElement { - MarkdownElement({this.rotation = 0, this.collection = '', @IdJsonConverter() this.id, @DoublePointJsonConverter() this.position = const Point(0.0, 0.0), this.scale = 1.0, this.styleSheet, this.areaProperty = const AreaProperty(), required this.text, this.constraint = const ElementConstraint(size: 1000), @ColorJsonConverter() this.foreground = SRGBColor.black, final Map extra = const {}, final String? $type}): _extra = extra,$type = $type ?? 'markdown',super._(); + MarkdownElement({this.rotation = 0, this.shear = 0, this.collection = '', @IdJsonConverter() this.id, @DoublePointJsonConverter() this.position = const Point(0.0, 0.0), this.scale = 1.0, this.styleSheet, this.areaProperty = const AreaProperty(), required this.text, this.constraint = const ElementConstraint(size: 1000), @ColorJsonConverter() this.foreground = SRGBColor.black, final Map extra = const {}, final String? $type}): _extra = extra,$type = $type ?? 'markdown',super._(); factory MarkdownElement.fromJson(Map json) => _$MarkdownElementFromJson(json); @override@JsonKey() final double rotation; +@override@JsonKey() final double shear; @override@JsonKey() final String collection; @override@IdJsonConverter() final String? id; @JsonKey()@DoublePointJsonConverter() final Point position; @@ -828,16 +834,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is MarkdownElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&(identical(other.position, position) || other.position == position)&&(identical(other.scale, scale) || other.scale == scale)&&(identical(other.styleSheet, styleSheet) || other.styleSheet == styleSheet)&&(identical(other.areaProperty, areaProperty) || other.areaProperty == areaProperty)&&(identical(other.text, text) || other.text == text)&&(identical(other.constraint, constraint) || other.constraint == constraint)&&(identical(other.foreground, foreground) || other.foreground == foreground)&&const DeepCollectionEquality().equals(other._extra, _extra)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is MarkdownElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.shear, shear) || other.shear == shear)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&(identical(other.position, position) || other.position == position)&&(identical(other.scale, scale) || other.scale == scale)&&(identical(other.styleSheet, styleSheet) || other.styleSheet == styleSheet)&&(identical(other.areaProperty, areaProperty) || other.areaProperty == areaProperty)&&(identical(other.text, text) || other.text == text)&&(identical(other.constraint, constraint) || other.constraint == constraint)&&(identical(other.foreground, foreground) || other.foreground == foreground)&&const DeepCollectionEquality().equals(other._extra, _extra)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,rotation,collection,id,position,scale,styleSheet,areaProperty,text,constraint,foreground,const DeepCollectionEquality().hash(_extra)); +int get hashCode => Object.hash(runtimeType,rotation,shear,collection,id,position,scale,styleSheet,areaProperty,text,constraint,foreground,const DeepCollectionEquality().hash(_extra)); @override String toString() { - return 'PadElement.markdown(rotation: $rotation, collection: $collection, id: $id, position: $position, scale: $scale, styleSheet: $styleSheet, areaProperty: $areaProperty, text: $text, constraint: $constraint, foreground: $foreground, extra: $extra)'; + return 'PadElement.markdown(rotation: $rotation, shear: $shear, collection: $collection, id: $id, position: $position, scale: $scale, styleSheet: $styleSheet, areaProperty: $areaProperty, text: $text, constraint: $constraint, foreground: $foreground, extra: $extra)'; } @@ -848,7 +854,7 @@ abstract mixin class $MarkdownElementCopyWith<$Res> implements $PadElementCopyWi factory $MarkdownElementCopyWith(MarkdownElement value, $Res Function(MarkdownElement) _then) = _$MarkdownElementCopyWithImpl; @override @useResult $Res call({ - double rotation, String collection,@IdJsonConverter() String? id,@DoublePointJsonConverter() Point position, double scale, NamedItem? styleSheet, AreaProperty areaProperty, String text, ElementConstraint constraint,@ColorJsonConverter() SRGBColor foreground, Map extra + double rotation, double shear, String collection,@IdJsonConverter() String? id,@DoublePointJsonConverter() Point position, double scale, NamedItem? styleSheet, AreaProperty areaProperty, String text, ElementConstraint constraint,@ColorJsonConverter() SRGBColor foreground, Map extra }); @@ -865,9 +871,10 @@ class _$MarkdownElementCopyWithImpl<$Res> /// Create a copy of PadElement /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? collection = null,Object? id = freezed,Object? position = null,Object? scale = null,Object? styleSheet = freezed,Object? areaProperty = null,Object? text = null,Object? constraint = null,Object? foreground = null,Object? extra = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? shear = null,Object? collection = null,Object? id = freezed,Object? position = null,Object? scale = null,Object? styleSheet = freezed,Object? areaProperty = null,Object? text = null,Object? constraint = null,Object? foreground = null,Object? extra = null,}) { return _then(MarkdownElement( rotation: null == rotation ? _self.rotation : rotation // ignore: cast_nullable_to_non_nullable +as double,shear: null == shear ? _self.shear : shear // ignore: cast_nullable_to_non_nullable as double,collection: null == collection ? _self.collection : collection // ignore: cast_nullable_to_non_nullable as String,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String?,position: null == position ? _self.position : position // ignore: cast_nullable_to_non_nullable @@ -919,10 +926,11 @@ $ElementConstraintCopyWith<$Res> get constraint { @JsonSerializable() class ImageElement extends PadElement implements SourcedElement { - ImageElement({this.rotation = 0, this.collection = '', @IdJsonConverter() this.id, @DoublePointJsonConverter() this.position = const Point(0.0, 0.0), this.constraints = const ScaledElementConstraints(scaleX: 1, scaleY: 1), required this.source, required this.width, required this.height, final Map extra = const {}, final String? $type}): _extra = extra,$type = $type ?? 'image',super._(); + ImageElement({this.rotation = 0, this.shear = 0, this.collection = '', @IdJsonConverter() this.id, @DoublePointJsonConverter() this.position = const Point(0.0, 0.0), this.constraints = const ScaledElementConstraints(scaleX: 1, scaleY: 1), required this.source, required this.width, required this.height, final Map extra = const {}, final String? $type}): _extra = extra,$type = $type ?? 'image',super._(); factory ImageElement.fromJson(Map json) => _$ImageElementFromJson(json); @override@JsonKey() final double rotation; +@override@JsonKey() final double shear; @override@JsonKey() final String collection; @override@IdJsonConverter() final String? id; @JsonKey()@DoublePointJsonConverter() final Point position; @@ -955,16 +963,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ImageElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&(identical(other.position, position) || other.position == position)&&(identical(other.constraints, constraints) || other.constraints == constraints)&&(identical(other.source, source) || other.source == source)&&(identical(other.width, width) || other.width == width)&&(identical(other.height, height) || other.height == height)&&const DeepCollectionEquality().equals(other._extra, _extra)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is ImageElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.shear, shear) || other.shear == shear)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&(identical(other.position, position) || other.position == position)&&(identical(other.constraints, constraints) || other.constraints == constraints)&&(identical(other.source, source) || other.source == source)&&(identical(other.width, width) || other.width == width)&&(identical(other.height, height) || other.height == height)&&const DeepCollectionEquality().equals(other._extra, _extra)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,rotation,collection,id,position,constraints,source,width,height,const DeepCollectionEquality().hash(_extra)); +int get hashCode => Object.hash(runtimeType,rotation,shear,collection,id,position,constraints,source,width,height,const DeepCollectionEquality().hash(_extra)); @override String toString() { - return 'PadElement.image(rotation: $rotation, collection: $collection, id: $id, position: $position, constraints: $constraints, source: $source, width: $width, height: $height, extra: $extra)'; + return 'PadElement.image(rotation: $rotation, shear: $shear, collection: $collection, id: $id, position: $position, constraints: $constraints, source: $source, width: $width, height: $height, extra: $extra)'; } @@ -975,7 +983,7 @@ abstract mixin class $ImageElementCopyWith<$Res> implements $PadElementCopyWith< factory $ImageElementCopyWith(ImageElement value, $Res Function(ImageElement) _then) = _$ImageElementCopyWithImpl; @override @useResult $Res call({ - double rotation, String collection,@IdJsonConverter() String? id,@DoublePointJsonConverter() Point position, ElementConstraints? constraints, String source, double width, double height, Map extra + double rotation, double shear, String collection,@IdJsonConverter() String? id,@DoublePointJsonConverter() Point position, ElementConstraints? constraints, String source, double width, double height, Map extra }); @@ -992,9 +1000,10 @@ class _$ImageElementCopyWithImpl<$Res> /// Create a copy of PadElement /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? collection = null,Object? id = freezed,Object? position = null,Object? constraints = freezed,Object? source = null,Object? width = null,Object? height = null,Object? extra = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? shear = null,Object? collection = null,Object? id = freezed,Object? position = null,Object? constraints = freezed,Object? source = null,Object? width = null,Object? height = null,Object? extra = null,}) { return _then(ImageElement( rotation: null == rotation ? _self.rotation : rotation // ignore: cast_nullable_to_non_nullable +as double,shear: null == shear ? _self.shear : shear // ignore: cast_nullable_to_non_nullable as double,collection: null == collection ? _self.collection : collection // ignore: cast_nullable_to_non_nullable as String,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String?,position: null == position ? _self.position : position // ignore: cast_nullable_to_non_nullable @@ -1026,10 +1035,11 @@ $ElementConstraintsCopyWith<$Res>? get constraints { @JsonSerializable() class PdfElement extends PadElement implements SourcedElement { - PdfElement({this.rotation = 0, this.collection = '', @IdJsonConverter() this.id, @DoublePointJsonConverter() this.position = const Point(0.0, 0.0), this.constraints = const ScaledElementConstraints(scaleX: 1, scaleY: 1), required this.source, this.page = 0, required this.width, required this.height, this.invert = false, @ColorJsonConverter() this.background = SRGBColor.transparent, final Map extra = const {}, final String? $type}): _extra = extra,$type = $type ?? 'pdf',super._(); + PdfElement({this.rotation = 0, this.shear = 0, this.collection = '', @IdJsonConverter() this.id, @DoublePointJsonConverter() this.position = const Point(0.0, 0.0), this.constraints = const ScaledElementConstraints(scaleX: 1, scaleY: 1), required this.source, this.page = 0, required this.width, required this.height, this.invert = false, @ColorJsonConverter() this.background = SRGBColor.transparent, final Map extra = const {}, final String? $type}): _extra = extra,$type = $type ?? 'pdf',super._(); factory PdfElement.fromJson(Map json) => _$PdfElementFromJson(json); @override@JsonKey() final double rotation; +@override@JsonKey() final double shear; @override@JsonKey() final String collection; @override@IdJsonConverter() final String? id; @JsonKey()@DoublePointJsonConverter() final Point position; @@ -1065,16 +1075,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is PdfElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&(identical(other.position, position) || other.position == position)&&(identical(other.constraints, constraints) || other.constraints == constraints)&&(identical(other.source, source) || other.source == source)&&(identical(other.page, page) || other.page == page)&&(identical(other.width, width) || other.width == width)&&(identical(other.height, height) || other.height == height)&&(identical(other.invert, invert) || other.invert == invert)&&(identical(other.background, background) || other.background == background)&&const DeepCollectionEquality().equals(other._extra, _extra)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is PdfElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.shear, shear) || other.shear == shear)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&(identical(other.position, position) || other.position == position)&&(identical(other.constraints, constraints) || other.constraints == constraints)&&(identical(other.source, source) || other.source == source)&&(identical(other.page, page) || other.page == page)&&(identical(other.width, width) || other.width == width)&&(identical(other.height, height) || other.height == height)&&(identical(other.invert, invert) || other.invert == invert)&&(identical(other.background, background) || other.background == background)&&const DeepCollectionEquality().equals(other._extra, _extra)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,rotation,collection,id,position,constraints,source,page,width,height,invert,background,const DeepCollectionEquality().hash(_extra)); +int get hashCode => Object.hash(runtimeType,rotation,shear,collection,id,position,constraints,source,page,width,height,invert,background,const DeepCollectionEquality().hash(_extra)); @override String toString() { - return 'PadElement.pdf(rotation: $rotation, collection: $collection, id: $id, position: $position, constraints: $constraints, source: $source, page: $page, width: $width, height: $height, invert: $invert, background: $background, extra: $extra)'; + return 'PadElement.pdf(rotation: $rotation, shear: $shear, collection: $collection, id: $id, position: $position, constraints: $constraints, source: $source, page: $page, width: $width, height: $height, invert: $invert, background: $background, extra: $extra)'; } @@ -1085,7 +1095,7 @@ abstract mixin class $PdfElementCopyWith<$Res> implements $PadElementCopyWith<$R factory $PdfElementCopyWith(PdfElement value, $Res Function(PdfElement) _then) = _$PdfElementCopyWithImpl; @override @useResult $Res call({ - double rotation, String collection,@IdJsonConverter() String? id,@DoublePointJsonConverter() Point position, ElementConstraints? constraints, String source, int page, double width, double height, bool invert,@ColorJsonConverter() SRGBColor background, Map extra + double rotation, double shear, String collection,@IdJsonConverter() String? id,@DoublePointJsonConverter() Point position, ElementConstraints? constraints, String source, int page, double width, double height, bool invert,@ColorJsonConverter() SRGBColor background, Map extra }); @@ -1102,9 +1112,10 @@ class _$PdfElementCopyWithImpl<$Res> /// Create a copy of PadElement /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? collection = null,Object? id = freezed,Object? position = null,Object? constraints = freezed,Object? source = null,Object? page = null,Object? width = null,Object? height = null,Object? invert = null,Object? background = null,Object? extra = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? shear = null,Object? collection = null,Object? id = freezed,Object? position = null,Object? constraints = freezed,Object? source = null,Object? page = null,Object? width = null,Object? height = null,Object? invert = null,Object? background = null,Object? extra = null,}) { return _then(PdfElement( rotation: null == rotation ? _self.rotation : rotation // ignore: cast_nullable_to_non_nullable +as double,shear: null == shear ? _self.shear : shear // ignore: cast_nullable_to_non_nullable as double,collection: null == collection ? _self.collection : collection // ignore: cast_nullable_to_non_nullable as String,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String?,position: null == position ? _self.position : position // ignore: cast_nullable_to_non_nullable @@ -1139,10 +1150,11 @@ $ElementConstraintsCopyWith<$Res>? get constraints { @JsonSerializable() class SvgElement extends PadElement implements SourcedElement { - SvgElement({this.rotation = 0, this.collection = '', @IdJsonConverter() this.id, @DoublePointJsonConverter() this.position = const Point(0.0, 0.0), this.constraints = const ScaledElementConstraints(scaleX: 1, scaleY: 1), required this.source, required this.width, required this.height, final Map extra = const {}, final String? $type}): _extra = extra,$type = $type ?? 'svg',super._(); + SvgElement({this.rotation = 0, this.shear = 0, this.collection = '', @IdJsonConverter() this.id, @DoublePointJsonConverter() this.position = const Point(0.0, 0.0), this.constraints = const ScaledElementConstraints(scaleX: 1, scaleY: 1), required this.source, required this.width, required this.height, final Map extra = const {}, final String? $type}): _extra = extra,$type = $type ?? 'svg',super._(); factory SvgElement.fromJson(Map json) => _$SvgElementFromJson(json); @override@JsonKey() final double rotation; +@override@JsonKey() final double shear; @override@JsonKey() final String collection; @override@IdJsonConverter() final String? id; @JsonKey()@DoublePointJsonConverter() final Point position; @@ -1175,16 +1187,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is SvgElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&(identical(other.position, position) || other.position == position)&&(identical(other.constraints, constraints) || other.constraints == constraints)&&(identical(other.source, source) || other.source == source)&&(identical(other.width, width) || other.width == width)&&(identical(other.height, height) || other.height == height)&&const DeepCollectionEquality().equals(other._extra, _extra)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is SvgElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.shear, shear) || other.shear == shear)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&(identical(other.position, position) || other.position == position)&&(identical(other.constraints, constraints) || other.constraints == constraints)&&(identical(other.source, source) || other.source == source)&&(identical(other.width, width) || other.width == width)&&(identical(other.height, height) || other.height == height)&&const DeepCollectionEquality().equals(other._extra, _extra)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,rotation,collection,id,position,constraints,source,width,height,const DeepCollectionEquality().hash(_extra)); +int get hashCode => Object.hash(runtimeType,rotation,shear,collection,id,position,constraints,source,width,height,const DeepCollectionEquality().hash(_extra)); @override String toString() { - return 'PadElement.svg(rotation: $rotation, collection: $collection, id: $id, position: $position, constraints: $constraints, source: $source, width: $width, height: $height, extra: $extra)'; + return 'PadElement.svg(rotation: $rotation, shear: $shear, collection: $collection, id: $id, position: $position, constraints: $constraints, source: $source, width: $width, height: $height, extra: $extra)'; } @@ -1195,7 +1207,7 @@ abstract mixin class $SvgElementCopyWith<$Res> implements $PadElementCopyWith<$R factory $SvgElementCopyWith(SvgElement value, $Res Function(SvgElement) _then) = _$SvgElementCopyWithImpl; @override @useResult $Res call({ - double rotation, String collection,@IdJsonConverter() String? id,@DoublePointJsonConverter() Point position, ElementConstraints? constraints, String source, double width, double height, Map extra + double rotation, double shear, String collection,@IdJsonConverter() String? id,@DoublePointJsonConverter() Point position, ElementConstraints? constraints, String source, double width, double height, Map extra }); @@ -1212,9 +1224,10 @@ class _$SvgElementCopyWithImpl<$Res> /// Create a copy of PadElement /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? collection = null,Object? id = freezed,Object? position = null,Object? constraints = freezed,Object? source = null,Object? width = null,Object? height = null,Object? extra = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? shear = null,Object? collection = null,Object? id = freezed,Object? position = null,Object? constraints = freezed,Object? source = null,Object? width = null,Object? height = null,Object? extra = null,}) { return _then(SvgElement( rotation: null == rotation ? _self.rotation : rotation // ignore: cast_nullable_to_non_nullable +as double,shear: null == shear ? _self.shear : shear // ignore: cast_nullable_to_non_nullable as double,collection: null == collection ? _self.collection : collection // ignore: cast_nullable_to_non_nullable as String,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String?,position: null == position ? _self.position : position // ignore: cast_nullable_to_non_nullable @@ -1246,10 +1259,11 @@ $ElementConstraintsCopyWith<$Res>? get constraints { @JsonSerializable() class ShapeElement extends PadElement { - ShapeElement({this.rotation = 0, this.collection = '', @IdJsonConverter() this.id, @DoublePointJsonConverter() this.firstPosition = const Point(0.0, 0.0), @DoublePointJsonConverter() this.secondPosition = const Point(0.0, 0.0), this.property = const ShapeProperty(shape: RectangleShape()), final Map extra = const {}, final String? $type}): _extra = extra,$type = $type ?? 'shape',super._(); + ShapeElement({this.rotation = 0, this.shear = 0, this.collection = '', @IdJsonConverter() this.id, @DoublePointJsonConverter() this.firstPosition = const Point(0.0, 0.0), @DoublePointJsonConverter() this.secondPosition = const Point(0.0, 0.0), this.property = const ShapeProperty(shape: RectangleShape()), final Map extra = const {}, final String? $type}): _extra = extra,$type = $type ?? 'shape',super._(); factory ShapeElement.fromJson(Map json) => _$ShapeElementFromJson(json); @override@JsonKey() final double rotation; +@override@JsonKey() final double shear; @override@JsonKey() final String collection; @override@IdJsonConverter() final String? id; @JsonKey()@DoublePointJsonConverter() final Point firstPosition; @@ -1280,16 +1294,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ShapeElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&(identical(other.firstPosition, firstPosition) || other.firstPosition == firstPosition)&&(identical(other.secondPosition, secondPosition) || other.secondPosition == secondPosition)&&const DeepCollectionEquality().equals(other.property, property)&&const DeepCollectionEquality().equals(other._extra, _extra)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is ShapeElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.shear, shear) || other.shear == shear)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&(identical(other.firstPosition, firstPosition) || other.firstPosition == firstPosition)&&(identical(other.secondPosition, secondPosition) || other.secondPosition == secondPosition)&&const DeepCollectionEquality().equals(other.property, property)&&const DeepCollectionEquality().equals(other._extra, _extra)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,rotation,collection,id,firstPosition,secondPosition,const DeepCollectionEquality().hash(property),const DeepCollectionEquality().hash(_extra)); +int get hashCode => Object.hash(runtimeType,rotation,shear,collection,id,firstPosition,secondPosition,const DeepCollectionEquality().hash(property),const DeepCollectionEquality().hash(_extra)); @override String toString() { - return 'PadElement.shape(rotation: $rotation, collection: $collection, id: $id, firstPosition: $firstPosition, secondPosition: $secondPosition, property: $property, extra: $extra)'; + return 'PadElement.shape(rotation: $rotation, shear: $shear, collection: $collection, id: $id, firstPosition: $firstPosition, secondPosition: $secondPosition, property: $property, extra: $extra)'; } @@ -1300,7 +1314,7 @@ abstract mixin class $ShapeElementCopyWith<$Res> implements $PadElementCopyWith< factory $ShapeElementCopyWith(ShapeElement value, $Res Function(ShapeElement) _then) = _$ShapeElementCopyWithImpl; @override @useResult $Res call({ - double rotation, String collection,@IdJsonConverter() String? id,@DoublePointJsonConverter() Point firstPosition,@DoublePointJsonConverter() Point secondPosition, ShapeProperty property, Map extra + double rotation, double shear, String collection,@IdJsonConverter() String? id,@DoublePointJsonConverter() Point firstPosition,@DoublePointJsonConverter() Point secondPosition, ShapeProperty property, Map extra }); @@ -1317,9 +1331,10 @@ class _$ShapeElementCopyWithImpl<$Res> /// Create a copy of PadElement /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? collection = null,Object? id = freezed,Object? firstPosition = null,Object? secondPosition = null,Object? property = freezed,Object? extra = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? shear = null,Object? collection = null,Object? id = freezed,Object? firstPosition = null,Object? secondPosition = null,Object? property = freezed,Object? extra = null,}) { return _then(ShapeElement( rotation: null == rotation ? _self.rotation : rotation // ignore: cast_nullable_to_non_nullable +as double,shear: null == shear ? _self.shear : shear // ignore: cast_nullable_to_non_nullable as double,collection: null == collection ? _self.collection : collection // ignore: cast_nullable_to_non_nullable as String,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String?,firstPosition: null == firstPosition ? _self.firstPosition : firstPosition // ignore: cast_nullable_to_non_nullable @@ -1337,10 +1352,11 @@ as Map, @JsonSerializable() class TextureElement extends PadElement { - TextureElement({this.rotation = 0, this.collection = '', @IdJsonConverter() this.id, this.texture = const SurfaceTexture.pattern(), @DoublePointJsonConverter() this.firstPosition = const Point(0.0, 0.0), @DoublePointJsonConverter() this.secondPosition = const Point(0.0, 0.0), final Map extra = const {}, final String? $type}): _extra = extra,$type = $type ?? 'texture',super._(); + TextureElement({this.rotation = 0, this.shear = 0, this.collection = '', @IdJsonConverter() this.id, this.texture = const SurfaceTexture.pattern(), @DoublePointJsonConverter() this.firstPosition = const Point(0.0, 0.0), @DoublePointJsonConverter() this.secondPosition = const Point(0.0, 0.0), final Map extra = const {}, final String? $type}): _extra = extra,$type = $type ?? 'texture',super._(); factory TextureElement.fromJson(Map json) => _$TextureElementFromJson(json); @override@JsonKey() final double rotation; +@override@JsonKey() final double shear; @override@JsonKey() final String collection; @override@IdJsonConverter() final String? id; @JsonKey() final SurfaceTexture texture; @@ -1371,16 +1387,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is TextureElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&(identical(other.texture, texture) || other.texture == texture)&&(identical(other.firstPosition, firstPosition) || other.firstPosition == firstPosition)&&(identical(other.secondPosition, secondPosition) || other.secondPosition == secondPosition)&&const DeepCollectionEquality().equals(other._extra, _extra)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is TextureElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.shear, shear) || other.shear == shear)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&(identical(other.texture, texture) || other.texture == texture)&&(identical(other.firstPosition, firstPosition) || other.firstPosition == firstPosition)&&(identical(other.secondPosition, secondPosition) || other.secondPosition == secondPosition)&&const DeepCollectionEquality().equals(other._extra, _extra)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,rotation,collection,id,texture,firstPosition,secondPosition,const DeepCollectionEquality().hash(_extra)); +int get hashCode => Object.hash(runtimeType,rotation,shear,collection,id,texture,firstPosition,secondPosition,const DeepCollectionEquality().hash(_extra)); @override String toString() { - return 'PadElement.texture(rotation: $rotation, collection: $collection, id: $id, texture: $texture, firstPosition: $firstPosition, secondPosition: $secondPosition, extra: $extra)'; + return 'PadElement.texture(rotation: $rotation, shear: $shear, collection: $collection, id: $id, texture: $texture, firstPosition: $firstPosition, secondPosition: $secondPosition, extra: $extra)'; } @@ -1391,7 +1407,7 @@ abstract mixin class $TextureElementCopyWith<$Res> implements $PadElementCopyWit factory $TextureElementCopyWith(TextureElement value, $Res Function(TextureElement) _then) = _$TextureElementCopyWithImpl; @override @useResult $Res call({ - double rotation, String collection,@IdJsonConverter() String? id, SurfaceTexture texture,@DoublePointJsonConverter() Point firstPosition,@DoublePointJsonConverter() Point secondPosition, Map extra + double rotation, double shear, String collection,@IdJsonConverter() String? id, SurfaceTexture texture,@DoublePointJsonConverter() Point firstPosition,@DoublePointJsonConverter() Point secondPosition, Map extra }); @@ -1408,9 +1424,10 @@ class _$TextureElementCopyWithImpl<$Res> /// Create a copy of PadElement /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? collection = null,Object? id = freezed,Object? texture = null,Object? firstPosition = null,Object? secondPosition = null,Object? extra = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? shear = null,Object? collection = null,Object? id = freezed,Object? texture = null,Object? firstPosition = null,Object? secondPosition = null,Object? extra = null,}) { return _then(TextureElement( rotation: null == rotation ? _self.rotation : rotation // ignore: cast_nullable_to_non_nullable +as double,shear: null == shear ? _self.shear : shear // ignore: cast_nullable_to_non_nullable as double,collection: null == collection ? _self.collection : collection // ignore: cast_nullable_to_non_nullable as String,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String?,texture: null == texture ? _self.texture : texture // ignore: cast_nullable_to_non_nullable @@ -1437,10 +1454,11 @@ $SurfaceTextureCopyWith<$Res> get texture { @JsonSerializable() class PolygonElement extends PadElement { - PolygonElement({this.rotation = 0, this.collection = '', @IdJsonConverter() this.id, final List points = const [], final Map extra = const {}, this.property = const PolygonProperty(), final String? $type}): _points = points,_extra = extra,$type = $type ?? 'polygon',super._(); + PolygonElement({this.rotation = 0, this.shear = 0, this.collection = '', @IdJsonConverter() this.id, final List points = const [], final Map extra = const {}, this.property = const PolygonProperty(), final String? $type}): _points = points,_extra = extra,$type = $type ?? 'polygon',super._(); factory PolygonElement.fromJson(Map json) => _$PolygonElementFromJson(json); @override@JsonKey() final double rotation; +@override@JsonKey() final double shear; @override@JsonKey() final String collection; @override@IdJsonConverter() final String? id; final List _points; @@ -1476,16 +1494,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is PolygonElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other._points, _points)&&const DeepCollectionEquality().equals(other._extra, _extra)&&const DeepCollectionEquality().equals(other.property, property)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is PolygonElement&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.shear, shear) || other.shear == shear)&&(identical(other.collection, collection) || other.collection == collection)&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other._points, _points)&&const DeepCollectionEquality().equals(other._extra, _extra)&&const DeepCollectionEquality().equals(other.property, property)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,rotation,collection,id,const DeepCollectionEquality().hash(_points),const DeepCollectionEquality().hash(_extra),const DeepCollectionEquality().hash(property)); +int get hashCode => Object.hash(runtimeType,rotation,shear,collection,id,const DeepCollectionEquality().hash(_points),const DeepCollectionEquality().hash(_extra),const DeepCollectionEquality().hash(property)); @override String toString() { - return 'PadElement.polygon(rotation: $rotation, collection: $collection, id: $id, points: $points, extra: $extra, property: $property)'; + return 'PadElement.polygon(rotation: $rotation, shear: $shear, collection: $collection, id: $id, points: $points, extra: $extra, property: $property)'; } @@ -1496,7 +1514,7 @@ abstract mixin class $PolygonElementCopyWith<$Res> implements $PadElementCopyWit factory $PolygonElementCopyWith(PolygonElement value, $Res Function(PolygonElement) _then) = _$PolygonElementCopyWithImpl; @override @useResult $Res call({ - double rotation, String collection,@IdJsonConverter() String? id, List points, Map extra, PolygonProperty property + double rotation, double shear, String collection,@IdJsonConverter() String? id, List points, Map extra, PolygonProperty property }); @@ -1513,9 +1531,10 @@ class _$PolygonElementCopyWithImpl<$Res> /// Create a copy of PadElement /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? collection = null,Object? id = freezed,Object? points = null,Object? extra = null,Object? property = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? rotation = null,Object? shear = null,Object? collection = null,Object? id = freezed,Object? points = null,Object? extra = null,Object? property = freezed,}) { return _then(PolygonElement( rotation: null == rotation ? _self.rotation : rotation // ignore: cast_nullable_to_non_nullable +as double,shear: null == shear ? _self.shear : shear // ignore: cast_nullable_to_non_nullable as double,collection: null == collection ? _self.collection : collection // ignore: cast_nullable_to_non_nullable as String,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String?,points: null == points ? _self._points : points // ignore: cast_nullable_to_non_nullable diff --git a/api/lib/src/models/element.g.dart b/api/lib/src/models/element.g.dart index b35bb2080330..919d886dc908 100644 --- a/api/lib/src/models/element.g.dart +++ b/api/lib/src/models/element.g.dart @@ -70,6 +70,7 @@ Map _$DynamicElementConstraintsToJson( PenElement _$PenElementFromJson(Map json) => PenElement( rotation: (json['rotation'] as num?)?.toDouble() ?? 0, + shear: (json['shear'] as num?)?.toDouble() ?? 0, collection: json['collection'] as String? ?? '', id: const IdJsonConverter().fromJson(json['id'] as String?), zoom: (json['zoom'] as num?)?.toDouble(), @@ -93,6 +94,7 @@ PenElement _$PenElementFromJson(Map json) => PenElement( Map _$PenElementToJson(PenElement instance) => { 'rotation': instance.rotation, + 'shear': instance.shear, 'collection': instance.collection, 'id': const IdJsonConverter().toJson(instance.id), 'zoom': instance.zoom, @@ -105,6 +107,7 @@ Map _$PenElementToJson(PenElement instance) => TextElement _$TextElementFromJson(Map json) => TextElement( rotation: (json['rotation'] as num?)?.toDouble() ?? 0, + shear: (json['shear'] as num?)?.toDouble() ?? 0, collection: json['collection'] as String? ?? '', id: const IdJsonConverter().fromJson(json['id'] as String?), position: json['position'] == null @@ -138,6 +141,7 @@ TextElement _$TextElementFromJson(Map json) => TextElement( Map _$TextElementToJson(TextElement instance) => { 'rotation': instance.rotation, + 'shear': instance.shear, 'collection': instance.collection, 'id': const IdJsonConverter().toJson(instance.id), 'position': const DoublePointJsonConverter().toJson(instance.position), @@ -152,6 +156,7 @@ Map _$TextElementToJson(TextElement instance) => MarkdownElement _$MarkdownElementFromJson(Map json) => MarkdownElement( rotation: (json['rotation'] as num?)?.toDouble() ?? 0, + shear: (json['shear'] as num?)?.toDouble() ?? 0, collection: json['collection'] as String? ?? '', id: const IdJsonConverter().fromJson(json['id'] as String?), position: json['position'] == null @@ -190,6 +195,7 @@ MarkdownElement _$MarkdownElementFromJson(Map json) => MarkdownElement( Map _$MarkdownElementToJson(MarkdownElement instance) => { 'rotation': instance.rotation, + 'shear': instance.shear, 'collection': instance.collection, 'id': const IdJsonConverter().toJson(instance.id), 'position': const DoublePointJsonConverter().toJson(instance.position), @@ -205,6 +211,7 @@ Map _$MarkdownElementToJson(MarkdownElement instance) => ImageElement _$ImageElementFromJson(Map json) => ImageElement( rotation: (json['rotation'] as num?)?.toDouble() ?? 0, + shear: (json['shear'] as num?)?.toDouble() ?? 0, collection: json['collection'] as String? ?? '', id: const IdJsonConverter().fromJson(json['id'] as String?), position: json['position'] == null @@ -227,6 +234,7 @@ ImageElement _$ImageElementFromJson(Map json) => ImageElement( Map _$ImageElementToJson(ImageElement instance) => { 'rotation': instance.rotation, + 'shear': instance.shear, 'collection': instance.collection, 'id': const IdJsonConverter().toJson(instance.id), 'position': const DoublePointJsonConverter().toJson(instance.position), @@ -240,6 +248,7 @@ Map _$ImageElementToJson(ImageElement instance) => PdfElement _$PdfElementFromJson(Map json) => PdfElement( rotation: (json['rotation'] as num?)?.toDouble() ?? 0, + shear: (json['shear'] as num?)?.toDouble() ?? 0, collection: json['collection'] as String? ?? '', id: const IdJsonConverter().fromJson(json['id'] as String?), position: json['position'] == null @@ -269,6 +278,7 @@ PdfElement _$PdfElementFromJson(Map json) => PdfElement( Map _$PdfElementToJson(PdfElement instance) => { 'rotation': instance.rotation, + 'shear': instance.shear, 'collection': instance.collection, 'id': const IdJsonConverter().toJson(instance.id), 'position': const DoublePointJsonConverter().toJson(instance.position), @@ -285,6 +295,7 @@ Map _$PdfElementToJson(PdfElement instance) => SvgElement _$SvgElementFromJson(Map json) => SvgElement( rotation: (json['rotation'] as num?)?.toDouble() ?? 0, + shear: (json['shear'] as num?)?.toDouble() ?? 0, collection: json['collection'] as String? ?? '', id: const IdJsonConverter().fromJson(json['id'] as String?), position: json['position'] == null @@ -307,6 +318,7 @@ SvgElement _$SvgElementFromJson(Map json) => SvgElement( Map _$SvgElementToJson(SvgElement instance) => { 'rotation': instance.rotation, + 'shear': instance.shear, 'collection': instance.collection, 'id': const IdJsonConverter().toJson(instance.id), 'position': const DoublePointJsonConverter().toJson(instance.position), @@ -320,6 +332,7 @@ Map _$SvgElementToJson(SvgElement instance) => ShapeElement _$ShapeElementFromJson(Map json) => ShapeElement( rotation: (json['rotation'] as num?)?.toDouble() ?? 0, + shear: (json['shear'] as num?)?.toDouble() ?? 0, collection: json['collection'] as String? ?? '', id: const IdJsonConverter().fromJson(json['id'] as String?), firstPosition: json['firstPosition'] == null @@ -344,6 +357,7 @@ ShapeElement _$ShapeElementFromJson(Map json) => ShapeElement( Map _$ShapeElementToJson(ShapeElement instance) => { 'rotation': instance.rotation, + 'shear': instance.shear, 'collection': instance.collection, 'id': const IdJsonConverter().toJson(instance.id), 'firstPosition': const DoublePointJsonConverter().toJson( @@ -359,6 +373,7 @@ Map _$ShapeElementToJson(ShapeElement instance) => TextureElement _$TextureElementFromJson(Map json) => TextureElement( rotation: (json['rotation'] as num?)?.toDouble() ?? 0, + shear: (json['shear'] as num?)?.toDouble() ?? 0, collection: json['collection'] as String? ?? '', id: const IdJsonConverter().fromJson(json['id'] as String?), texture: json['texture'] == null @@ -383,6 +398,7 @@ TextureElement _$TextureElementFromJson(Map json) => TextureElement( Map _$TextureElementToJson(TextureElement instance) => { 'rotation': instance.rotation, + 'shear': instance.shear, 'collection': instance.collection, 'id': const IdJsonConverter().toJson(instance.id), 'texture': instance.texture.toJson(), @@ -398,6 +414,7 @@ Map _$TextureElementToJson(TextureElement instance) => PolygonElement _$PolygonElementFromJson(Map json) => PolygonElement( rotation: (json['rotation'] as num?)?.toDouble() ?? 0, + shear: (json['shear'] as num?)?.toDouble() ?? 0, collection: json['collection'] as String? ?? '', id: const IdJsonConverter().fromJson(json['id'] as String?), points: @@ -421,6 +438,7 @@ PolygonElement _$PolygonElementFromJson(Map json) => PolygonElement( Map _$PolygonElementToJson(PolygonElement instance) => { 'rotation': instance.rotation, + 'shear': instance.shear, 'collection': instance.collection, 'id': const IdJsonConverter().toJson(instance.id), 'points': instance.points.map((e) => e.toJson()).toList(), diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 3a4d5ced0754..6e6c5153a910 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -1515,5 +1515,6 @@ "showPenOnlyToggleDescription": "Shows a quick pen-only switch in the editor after Butterfly detects a pen.", "ignorePressureDescription": "Controls whether pen pressure changes the stroke and works around inaccurate pressure readings from some pens.", "moveOnGestureDescription": "Lets multi-touch gestures move the canvas instead of interacting with note content.", - "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page." + "spreadPagesDescription": "Splits multi-page imports across separate note pages instead of placing everything on one page.", + "shear": "Shear" } diff --git a/app/lib/renderers/elements/image.dart b/app/lib/renderers/elements/image.dart index 170274d02e50..39d8547b000a 100644 --- a/app/lib/renderers/elements/image.dart +++ b/app/lib/renderers/elements/image.dart @@ -179,6 +179,7 @@ class ImageRenderer extends Renderer { ImageRenderer _transform({ required Offset position, required double rotation, + required double shear, double scaleX = 1, double scaleY = 1, }) { @@ -186,6 +187,7 @@ class ImageRenderer extends Renderer { element.copyWith( position: position.toPoint(), rotation: rotation, + shear: shear, constraints: element.constraints.scale(scaleX, scaleY), ), layer, diff --git a/app/lib/renderers/elements/markdown.dart b/app/lib/renderers/elements/markdown.dart index 06762110da53..402bc9c32398 100644 --- a/app/lib/renderers/elements/markdown.dart +++ b/app/lib/renderers/elements/markdown.dart @@ -81,12 +81,14 @@ class MarkdownRenderer extends GenericTextRenderer { MarkdownRenderer _transform({ required Offset position, required double rotation, + required double shear, double scaleX = 1, double scaleY = 1, }) => MarkdownRenderer( element.copyWith( position: position.toPoint(), rotation: rotation, + shear: shear, scale: element.scale * max(scaleX, scaleY), ), layer, diff --git a/app/lib/renderers/elements/pdf.dart b/app/lib/renderers/elements/pdf.dart index 4323c5fecc7a..15df82a06f96 100644 --- a/app/lib/renderers/elements/pdf.dart +++ b/app/lib/renderers/elements/pdf.dart @@ -279,6 +279,7 @@ class PdfRenderer extends Renderer { PdfRenderer _transform({ required Offset position, required double rotation, + required double shear, double scaleX = 1, double scaleY = 1, }) { @@ -286,6 +287,7 @@ class PdfRenderer extends Renderer { element.copyWith( position: position.toPoint(), rotation: rotation, + shear: shear, constraints: element.constraints.scale(scaleX, scaleY), ), layer, diff --git a/app/lib/renderers/elements/pen.dart b/app/lib/renderers/elements/pen.dart index dd48eec5e9d3..4438d3eb469d 100644 --- a/app/lib/renderers/elements/pen.dart +++ b/app/lib/renderers/elements/pen.dart @@ -8,11 +8,22 @@ class PenRenderer extends Renderer { super.element, [ super.layer, this.rect = Rect.zero, - this.expandedRect = Rect.zero, - ]); + Rect expandedRect = Rect.zero, + ]) : expandedRect = expandedRect, + _localExpandedRect = expandedRect { + if (rotation != 0 || shear != 0) { + _localExpandedRect = Renderer._inverseAabbFor( + expandedRect, + rect.center, + rotation * pi / 180, + shear, + ); + } + } @override Rect expandedRect; + Rect _localExpandedRect; Path? _cachedFillPath; Path? _cachedStrokePath; @@ -100,13 +111,7 @@ class PenRenderer extends Renderer { bottomRightCorner.dx, bottomRightCorner.dy, ); - final center = Rect.fromPoints(topLeftCorner, bottomRightCorner).center; - final rotatedPoints = points - .map((e) => e.rotate(center, rotation / 180 * pi)) - .toList(); - topLeftCorner = rotatedPoints.first.toOffset(); - bottomRightCorner = rotatedPoints.first.toOffset(); - for (final element in rotatedPoints) { + for (final element in points) { final width = property.strokeWidth + element.pressure * property.thinning; topLeftCorner = Offset( min(topLeftCorner.dx, element.x - width), @@ -117,12 +122,17 @@ class PenRenderer extends Renderer { max(bottomRightCorner.dy, element.y + width), ); } - expandedRect = Rect.fromLTRB( + _localExpandedRect = Rect.fromLTRB( topLeftCorner.dx, topLeftCorner.dy, bottomRightCorner.dx, bottomRightCorner.dy, ); + expandedRect = Renderer._expandedAabbFor( + _localExpandedRect, + rotation / 180 * pi, + shear, + ); await Future.wait([ _strokePaint.setup(property.paint, document, assetService), _fillPaint.setup(property.fillPaint, document, assetService), @@ -311,11 +321,13 @@ class PenRenderer extends Renderer { PenRenderer _transform({ required Offset position, required double rotation, + required double shear, double scaleX = 1, double scaleY = 1, }) => PenRenderer( element.copyWith( rotation: rotation, + shear: shear, points: movePoints(position, scaleX, scaleY), ), layer, @@ -324,12 +336,12 @@ class PenRenderer extends Renderer { ); @override - PathHitCalculator getHitCalculator() { + PathHitCalculator createHitCalculator() { _cachedHitCalculator ??= PathHitCalculator( rect, - expandedRect, + _localExpandedRect, element.points, - rotation * pi / 180, + 0, ); return _cachedHitCalculator!; } diff --git a/app/lib/renderers/elements/polygon.dart b/app/lib/renderers/elements/polygon.dart index 1d47069bf9c5..f54b926d39a2 100644 --- a/app/lib/renderers/elements/polygon.dart +++ b/app/lib/renderers/elements/polygon.dart @@ -45,6 +45,7 @@ class PolygonRenderer extends Renderer { : 0, ), rotation * pi / 180, + shear, ); void _computePath() { @@ -228,11 +229,11 @@ class PolygonRenderer extends Renderer { } @override - PolygonHitCalculator getHitCalculator() { + PolygonHitCalculator createHitCalculator() { _cachedHitCalculator ??= PolygonHitCalculator( rect, element.points, - rotation * pi / 180, + 0, element.property, ); return _cachedHitCalculator!; @@ -242,11 +243,13 @@ class PolygonRenderer extends Renderer { PolygonRenderer _transform({ required Offset position, required double rotation, + required double shear, double scaleX = 1, double scaleY = 1, }) => PolygonRenderer( element.copyWith( rotation: rotation, + shear: shear, points: movePoints(position, scaleX, scaleY), ), layer, diff --git a/app/lib/renderers/elements/shape.dart b/app/lib/renderers/elements/shape.dart index 36e35258e3f4..d1c03d8f56b8 100644 --- a/app/lib/renderers/elements/shape.dart +++ b/app/lib/renderers/elements/shape.dart @@ -54,7 +54,7 @@ class ShapeRenderer extends Renderer { final expanded = rect.isEmpty ? rect.inflate(max(element.property.strokeWidth / 2, 1)) : rect; - return Renderer._expandedAabbFor(expanded, rotation * pi / 180); + return Renderer._expandedAabbFor(expanded, rotation * pi / 180, shear); } /// Creates a dotted path from the source path based on stroke style @@ -346,6 +346,7 @@ class ShapeRenderer extends Renderer { ShapeRenderer _transform({ required Offset position, required double rotation, + required double shear, double scaleX = 1, double scaleY = 1, }) { @@ -355,6 +356,7 @@ class ShapeRenderer extends Renderer { final localSecond = element.secondPosition.toOffset() - previous; return ShapeRenderer( element.copyWith( + shear: shear, firstPosition: (localFirst.scale(scaleX, scaleY) + position).toPoint(), secondPosition: (localSecond.scale(scaleX, scaleY) + position) .toPoint(), @@ -365,8 +367,12 @@ class ShapeRenderer extends Renderer { } @override - HitCalculator getHitCalculator() => - ShapeHitCalculator(element, rect, expandedRect, rotation * pi / 180); + HitCalculator createHitCalculator() { + final bounds = rect.isEmpty + ? rect.inflate(max(element.property.strokeWidth / 2, 1)) + : rect; + return ShapeHitCalculator(element, rect, bounds, 0); + } } class ShapeHitCalculator extends HitCalculator { diff --git a/app/lib/renderers/elements/svg.dart b/app/lib/renderers/elements/svg.dart index d5ce7ca8477d..255b812648b3 100644 --- a/app/lib/renderers/elements/svg.dart +++ b/app/lib/renderers/elements/svg.dart @@ -147,6 +147,7 @@ class SvgRenderer extends Renderer { SvgRenderer _transform({ required Offset position, required double rotation, + required double shear, double scaleX = 1, double scaleY = 1, }) { @@ -154,6 +155,7 @@ class SvgRenderer extends Renderer { element.copyWith( position: position.toPoint(), rotation: rotation, + shear: shear, width: element.width * scaleX, height: element.height * scaleY, ), diff --git a/app/lib/renderers/elements/text.dart b/app/lib/renderers/elements/text.dart index 9321083c68ac..4fcdc641cc16 100644 --- a/app/lib/renderers/elements/text.dart +++ b/app/lib/renderers/elements/text.dart @@ -370,12 +370,14 @@ class TextRenderer extends GenericTextRenderer { TextRenderer _transform({ required Offset position, required double rotation, + required double shear, double scaleX = 1, double scaleY = 1, }) => TextRenderer( element.copyWith( position: position.toPoint(), rotation: rotation, + shear: shear, scale: element.scale * max(scaleX, scaleY), ), layer, diff --git a/app/lib/renderers/elements/texture.dart b/app/lib/renderers/elements/texture.dart index 41161adbe87e..4184a6e7826c 100644 --- a/app/lib/renderers/elements/texture.dart +++ b/app/lib/renderers/elements/texture.dart @@ -46,6 +46,7 @@ class TextureRenderer extends Renderer { TextureRenderer _transform({ required Offset position, required double rotation, + required double shear, double scaleX = 1, double scaleY = 1, }) { @@ -55,6 +56,7 @@ class TextureRenderer extends Renderer { (element.firstPosition.y - element.secondPosition.y).abs() * scaleY; return TextureRenderer( element.copyWith( + shear: shear, firstPosition: position.toPoint(), secondPosition: position.translate(sizeX, sizeY).toPoint(), rotation: rotation, diff --git a/app/lib/renderers/renderer.dart b/app/lib/renderers/renderer.dart index 3cbfdc9893d8..c4829b206f10 100644 --- a/app/lib/renderers/renderer.dart +++ b/app/lib/renderers/renderer.dart @@ -222,6 +222,64 @@ class DefaultHitCalculator extends HitCalculator { } } +class TransformedHitCalculator extends HitCalculator { + final HitCalculator delegate; + final Rect? bounds; + final Offset center; + final double rotation; + final double shear; + + TransformedHitCalculator( + this.delegate, + this.bounds, + this.center, + this.rotation, + this.shear, + ); + + Offset _inverse(Offset point) { + final rotated = (point - center).rotate(Offset.zero, -rotation); + return center + Offset(rotated.dx - rotated.dy * shear, rotated.dy); + } + + @override + bool hit( + Rect rect, { + HitElementMode hitElementMode = HitElementMode.touchAnywhere, + }) { + if (!isFiniteRect(rect)) { + final bounds = this.bounds; + if (bounds == null) return false; + return hitElementMode == HitElementMode.full + ? [ + bounds.topLeft, + bounds.topRight, + bounds.bottomRight, + bounds.bottomLeft, + ].every(rect.contains) + : bounds.overlaps(rect); + } + return delegate.hitPolygon( + [ + rect.topLeft, + rect.topRight, + rect.bottomRight, + rect.bottomLeft, + ].map(_inverse).toList(), + hitElementMode: hitElementMode, + ); + } + + @override + bool hitPolygon( + List polygon, { + HitElementMode hitElementMode = HitElementMode.touchAnywhere, + }) => delegate.hitPolygon( + polygon.map(_inverse).toList(), + hitElementMode: hitElementMode, + ); +} + /// A helper class to represent the projection of a polygon onto an axis. class Projection { double min; @@ -461,6 +519,8 @@ abstract class Renderer { double get rotation => element is PadElement ? (element as PadElement).rotation : 0.0; + double get shear => element is PadElement ? (element as PadElement).shear : 0; + String get id => (element is PadElement ? (element as PadElement).id : null) ?? createUniqueId(); @@ -500,17 +560,37 @@ abstract class Renderer { final current = rect; if (current == null) return null; final rotation = this.rotation * (pi / 180); - return _expandedAabbFor(current, rotation); + return _expandedAabbFor(current, rotation, shear); + } + + static Offset _transformPoint( + Offset point, + Offset center, + double radians, + double shear, + ) { + final local = point - center; + final sheared = Offset(local.dx + local.dy * shear, local.dy); + return center + sheared.rotate(Offset.zero, radians); } - // Computes the axis-aligned bounding box of a rotated rect. - static Rect _expandedAabbFor(Rect r, double radians) { - if (radians == 0) return r; + Offset transformPoint(Offset point, [Rect? bounds]) => _transformPoint( + point, + (bounds ?? rect ?? Rect.zero).center, + rotation * pi / 180, + shear, + ); + + // Computes the axis-aligned bounding box of an affine-transformed rect. + static Rect _expandedAabbFor(Rect r, double radians, [double shear = 0]) { + if (radians == 0 && shear == 0) { + return r; + } final center = r.center; - final topLeft = r.topLeft.rotate(center, radians); - final topRight = r.topRight.rotate(center, radians); - final bottomLeft = r.bottomLeft.rotate(center, radians); - final bottomRight = r.bottomRight.rotate(center, radians); + final topLeft = _transformPoint(r.topLeft, center, radians, shear); + final topRight = _transformPoint(r.topRight, center, radians, shear); + final bottomLeft = _transformPoint(r.bottomLeft, center, radians, shear); + final bottomRight = _transformPoint(r.bottomRight, center, radians, shear); final all = [topLeft, topRight, bottomLeft, bottomRight]; final xs = all.map((p) => p.dx); final ys = all.map((p) => p.dy); @@ -521,6 +601,45 @@ abstract class Renderer { return Rect.fromLTRB(left, top, right, bottom); } + static Rect _inverseAabbFor( + Rect r, + Offset center, + double radians, + double shear, + ) { + Offset inverse(Offset point) { + final rotated = (point - center).rotate(Offset.zero, -radians); + return center + Offset(rotated.dx - rotated.dy * shear, rotated.dy); + } + + final points = [ + inverse(r.topLeft), + inverse(r.topRight), + inverse(r.bottomRight), + inverse(r.bottomLeft), + ]; + return Rect.fromLTRB( + points.map((point) => point.dx).reduce(min), + points.map((point) => point.dy).reduce(min), + points.map((point) => point.dx).reduce(max), + points.map((point) => point.dy).reduce(max), + ); + } + + bool transformCanvas(Canvas canvas) { + final radians = rotation * pi / 180; + if (radians == 0 && shear == 0) { + return false; + } + final center = rect?.center; + canvas.save(); + if (center != null) canvas.translate(center.dx, center.dy); + canvas.rotate(radians); + canvas.transform((Matrix4.identity()..setEntry(0, 1, shear)).storage); + if (center != null) canvas.translate(-center.dx, -center.dy); + return true; + } + void build( Canvas canvas, Size size, @@ -532,8 +651,20 @@ abstract class Renderer { bool foreground = false, ]); - HitCalculator getHitCalculator() => - DefaultHitCalculator(rect, expandedRect, rotation * (pi / 180)); + HitCalculator getHitCalculator() { + final calculator = createHitCalculator(); + if (rotation == 0 && shear == 0) return calculator; + return TransformedHitCalculator( + calculator, + expandedRect, + rect?.center ?? Offset.zero, + rotation * pi / 180, + shear, + ); + } + + @protected + HitCalculator createHitCalculator() => DefaultHitCalculator(rect, rect, 0); void buildSvg( XmlDocument xml, @@ -552,8 +683,7 @@ abstract class Renderer { }) { final rect = this.rect ?? Rect.zero; rotation ??= relative ? 0 : this.rotation; - final double nextRotation = - (relative ? rotation + this.rotation : rotation) % 360; + final rotationDelta = relative ? rotation : rotation - this.rotation; // Determine position (absolute for _transform) position ??= relative ? Offset.zero : rect.topLeft; @@ -567,93 +697,66 @@ abstract class Renderer { nextPosition = relativePosition + rect.topLeft; } - // Convert world axis-aligned scaling into local-space scaling. - // The expanded (AABB) rect is what gets scaled in world space, so we solve - // for the local scale factors that produce the desired AABB dimensions. - // - // The AABB dimensions of a rect (w, h) rotated by θ are: - // EW = w·|cosθ| + h·|sinθ| - // EH = w·|sinθ| + h·|cosθ| - // - // After applying local scales (sx, sy), the new AABB must satisfy: - // sx·w·|cosθ| + sy·h·|sinθ| = scaleX · EW - // sx·w·|sinθ| + sy·h·|cosθ| = scaleY · EH - // - // This 2×2 system has determinant w·h·cos(2θ). When cos(2θ) ≈ 0 - // (rotation near 45°/135°) or the exact solution yields negative local - // scales, we fall back to the diagonal projection of the world-space - // scale matrix onto the local axes. - double sx = scaleX; - double sy = scaleY; - if ((scaleX != 1 || scaleY != 1) && (this.rotation % 360) != 0) { - final w = rect.width; - final h = rect.height; - final expandedRect = _expandedAabbFor(rect, radians); - - if (w > 0 && h > 0) { - final absC = cos(radians).abs(); - final absS = sin(radians).abs(); - final cos2theta = absC * absC - absS * absS; // cos(2θ) - - if (cos2theta.abs() > 1e-10) { - // Expanded AABB dimensions of the current (unscaled) element. - final ew = w * absC + h * absS; - final eh = w * absS + h * absC; - - // Solve the 2×2 linear system for exact local scales. - final exactSx = - (absC * scaleX * ew - absS * scaleY * eh) / (w * cos2theta); - final exactSy = - (absC * scaleY * eh - absS * scaleX * ew) / (h * cos2theta); - - if (exactSx > 0 && exactSy > 0) { - sx = exactSx; - sy = exactSy; - } else { - // Exact solution requires a negative local scale (not - // representable). Fall back to diagonal projection. - final c2 = cos(radians) * cos(radians); - final s2 = sin(radians) * sin(radians); - sx = scaleX * c2 + scaleY * s2; - sy = scaleX * s2 + scaleY * c2; - } - } else { - // Near 45°/135°: system is ill-conditioned. Use diagonal - // projection (weighted average of world scales). - final c2 = cos(radians) * cos(radians); - final s2 = sin(radians) * sin(radians); - sx = scaleX * c2 + scaleY * s2; - sy = scaleX * s2 + scaleY * c2; - } - } - - // Position compensation: the offset between rect.topLeft and its - // expanded AABB changes when local scales change. Adjust so the - // expanded AABB stays anchored correctly. - final Offset fOld = expandedRect.topLeft - rect.topLeft; - final Rect scaledRect = Rect.fromLTWH( - rect.left, - rect.top, - w * sx, - h * sy, - ); - final Rect newExpanded = _expandedAabbFor(scaledRect, radians); - final Offset fNew = newExpanded.topLeft - scaledRect.topLeft; - - nextPosition += (fOld - fNew); + // Compose the requested world transform with the current local transform. + // QR decomposition then gives a rotation, an X shear and local scales, + // which together can represent every non-reflecting affine transform. + final currentShear = shear; + final c = cos(radians), s = sin(radians); + final a = c; + final b = c * currentShear - s; + final d = s; + final e = s * currentShear + c; + final deltaRadians = rotationDelta * pi / 180; + final dc = cos(deltaRadians), ds = sin(deltaRadians); + final m00 = dc * scaleX * a - ds * scaleY * d; + final m01 = dc * scaleX * b - ds * scaleY * e; + final m10 = ds * scaleX * a + dc * scaleY * d; + final m11 = ds * scaleX * b + dc * scaleY * e; + + final r00 = sqrt(m00 * m00 + m10 * m10); + if (r00 <= 1e-12) return null; + final q00 = m00 / r00, q10 = m10 / r00; + final r01 = q00 * m01 + q10 * m11; + final determinant = m00 * m11 - m01 * m10; + final r11 = determinant / r00; + if (r11.abs() <= 1e-12) return null; + + final nextRotation = atan2(q10, q00) * 180 / pi % 360; + final geometryScaleX = r00; + final geometryScaleY = r11.abs(); + final nextShear = r01 / r11; + + final oldExpanded = _expandedAabbFor(rect, radians, currentShear); + final scaledRect = Rect.fromLTWH( + rect.left, + rect.top, + rect.width * geometryScaleX, + rect.height * geometryScaleY, + ); + final newExpanded = _expandedAabbFor( + scaledRect, + nextRotation * pi / 180, + nextShear, + ); + if (rotationDelta == 0) { + nextPosition += + (oldExpanded.topLeft - rect.topLeft) - + (newExpanded.topLeft - scaledRect.topLeft); } return _transform( position: nextPosition, rotation: nextRotation, - scaleX: sx, - scaleY: sy, + shear: nextShear, + scaleX: geometryScaleX, + scaleY: geometryScaleY, ); } Renderer? _transform({ required Offset position, required double rotation, + required double shear, // ignore: unused_element_parameter double scaleX = 1, // ignore: unused_element_parameter diff --git a/app/lib/selections/elements/element.dart b/app/lib/selections/elements/element.dart index 53c021d32e6f..b6379ce4db01 100644 --- a/app/lib/selections/elements/element.dart +++ b/app/lib/selections/elements/element.dart @@ -102,6 +102,23 @@ class ElementSelection extends Selection> { ); }, ), + ExactSlider( + value: atan(elements.first.shear) * 180 / pi, + defaultValue: 0, + min: -90, + max: 90, + header: Text(AppLocalizations.of(context).shear), + onChangeEnd: (value) => updateElements( + context, + selected + .map( + (renderer) => + renderer.element.copyWith(shear: tan(value * pi / 180)), + ) + .whereType() + .toList(), + ), + ), ]; } diff --git a/app/lib/view_painter.dart b/app/lib/view_painter.dart index b4cab90feeb5..31d5d59654c9 100644 --- a/app/lib/view_painter.dart +++ b/app/lib/view_painter.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/helpers/rect.dart'; @@ -26,18 +24,7 @@ void _paintRenderer( bool foreground = false, bool combined = false, }) { - final rotation = renderer.rotation; - if (rotation != 0) { - canvas.save(); - final center = renderer.rect?.center; - if (center != null) { - canvas.translate(center.dx, center.dy); - } - canvas.rotate(rotation * (pi / 180)); - if (center != null) { - canvas.translate(-center.dx, -center.dy); - } - } + final transformed = renderer.transformCanvas(canvas); if (combined && renderer is PenRenderer) { renderer.buildCombined(canvas); } else { @@ -52,9 +39,7 @@ void _paintRenderer( foreground, ); } - if (rotation != 0) { - canvas.restore(); - } + if (transformed) canvas.restore(); } Rect? _rendererBounds(Renderer renderer) => renderer.expandedRect; diff --git a/app/test/renderers/shape_renderer_test.dart b/app/test/renderers/shape_renderer_test.dart index 659f097d2143..ec82423b9fca 100644 --- a/app/test/renderers/shape_renderer_test.dart +++ b/app/test/renderers/shape_renderer_test.dart @@ -63,6 +63,39 @@ void main() { }); group('rotation test', () { + test('non-uniform scaling preserves the affine shape of a rotation', () { + final renderer = ShapeRenderer( + ShapeElement( + rotation: 45, + firstPosition: const Point(0, 0), + secondPosition: const Point(100, 50), + ), + ); + + final transformed = renderer.transform(scaleX: 2, scaleY: 1)!; + final rect = transformed.rect!; + final horizontal = + transformed.transformPoint(rect.topRight) - + transformed.transformPoint(rect.topLeft); + final vertical = + transformed.transformPoint(rect.bottomLeft) - + transformed.transformPoint(rect.topLeft); + final expectedHorizontal = Offset(200 * cos(pi / 4), 100 * sin(pi / 4)); + final expectedVertical = Offset(-100 * sin(pi / 4), 50 * cos(pi / 4)); + + expect(transformed.shear, isNot(0)); + expect(horizontal.dx, closeTo(expectedHorizontal.dx, 1e-9)); + expect(horizontal.dy, closeTo(expectedHorizontal.dy, 1e-9)); + expect(vertical.dx, closeTo(expectedVertical.dx, 1e-9)); + expect(vertical.dy, closeTo(expectedVertical.dy, 1e-9)); + }); + + test('documents without shear keep the identity default', () { + final json = ShapeElement().toJson()..remove('shear'); + + expect(PadElement.fromJson(json).shear, 0); + }); + test('rotated pen stroke is hit outside its original bounds', () { final hitCalculator = PenRenderer( PenElement( diff --git a/metadata/en-US/changelogs/188.txt b/metadata/en-US/changelogs/188.txt index 57e0a0e032a6..945728b3f85d 100644 --- a/metadata/en-US/changelogs/188.txt +++ b/metadata/en-US/changelogs/188.txt @@ -1,4 +1,5 @@ * Add persistent document states ([#1077](https://github.com/LinwoodDev/Butterfly/issues/1077)) +* Add shear property for elements * Reorder top corner menu to have home on top ([#1161](https://github.com/LinwoodDev/Butterfly/issues/1161)) * Rebuild internal settings pages * Add search bar to settings pages ([#1158](https://github.com/LinwoodDev/Butterfly/issues/1158)) @@ -6,6 +7,7 @@ * Add settings descriptions * Refactor whole state management structure ([#1157](https://github.com/LinwoodDev/Butterfly/pull/1157)) * Remove unused view options +* Improve rotated elements transformtion ([#1099](https://github.com/LinwoodDev/Butterfly/issues/1099)) * Fix crash with android saf on folders with many files * Fix blur resetting on color change * Fix polygon collision aabb tests if closed ([#1162](https://github.com/LinwoodDev/Butterfly/pull/1162)) From df01580b050952e050e0002f905bc764181140bc Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Wed, 15 Jul 2026 12:12:13 +0200 Subject: [PATCH 089/117] Add camera rotation support, closes #977 --- app/lib/cubits/editor_renderer.dart | 31 ++----- app/lib/cubits/editor_session.dart | 2 + app/lib/cubits/transform.dart | 83 ++++++++++++++++--- app/lib/cubits/transform.freezed.dart | 31 +++---- app/lib/models/persisted_document_state.dart | 2 + .../persisted_document_state.freezed.dart | 54 ++++++------ .../models/persisted_document_state.g.dart | 4 + app/lib/renderers/backgrounds/image.dart | 20 ++--- app/lib/renderers/backgrounds/svg.dart | 25 ++---- app/lib/renderers/backgrounds/texture.dart | 21 ++--- app/lib/selections/document.dart | 23 +++++ app/lib/view_painter.dart | 52 ++++-------- app/lib/views/edit.dart | 8 ++ app/lib/views/main.dart | 1 + app/lib/views/view.dart | 44 +++++++--- app/test/cubits/editor_session_test.dart | 7 +- app/test/cubits/transform_test.dart | 64 ++++++++++++++ metadata/en-US/changelogs/188.txt | 3 + 18 files changed, 309 insertions(+), 166 deletions(-) create mode 100644 app/test/cubits/transform_test.dart diff --git a/app/lib/cubits/editor_renderer.dart b/app/lib/cubits/editor_renderer.dart index 5592c886b49c..a33155b55b0d 100644 --- a/app/lib/cubits/editor_renderer.dart +++ b/app/lib/cubits/editor_renderer.dart @@ -132,25 +132,14 @@ class RendererCubit extends Cubit { final transform = transformCubit.state; final resolution = settingsCubit.state.renderResolution; final friction = transform.friction; - final realWidth = size.width / transform.size; - final realHeight = size.height / transform.size; - Rect rect = Rect.fromLTWH( - transform.position.dx, - transform.position.dy, - realWidth, - realHeight, - ); + Rect rect = transform.localToGlobalRect(Offset.zero & size); if (friction != null) { final beginPosition = transform.position - friction.beginOffset; - final topLeft = Offset( - min(transform.position.dx, beginPosition.dx), - min(transform.position.dy, beginPosition.dy), - ); - final frictionSize = Size( - realWidth + (friction.beginOffset.dx * transform.size).abs(), - realHeight + (friction.beginOffset.dy * transform.size).abs(), + rect = rect.expandToInclude( + transform + .withPosition(beginPosition) + .localToGlobalRect(Offset.zero & size), ); - rect = topLeft & frictionSize; } return _snapViewportRect(rect, size, transform, resolution); } @@ -161,10 +150,7 @@ class RendererCubit extends Cubit { CameraTransform transform, RenderResolution resolution, ) { - final screenRect = Rect.fromPoints( - transform.globalToLocal(rect.topLeft), - transform.globalToLocal(rect.bottomRight), - ); + final screenRect = transform.globalToLocalRect(rect); final snappedRect = _expandScreenRect( Rect.fromLTRB( screenRect.left.floorToDouble(), @@ -177,10 +163,7 @@ class RendererCubit extends Cubit { (size.height * resolution.multiplier).ceilToDouble(), ), ); - return Rect.fromPoints( - transform.localToGlobal(snappedRect.topLeft), - transform.localToGlobal(snappedRect.bottomRight), - ); + return transform.localToGlobalRect(snappedRect); } Rect _expandScreenRect(Rect rect, Size minimumSize) { diff --git a/app/lib/cubits/editor_session.dart b/app/lib/cubits/editor_session.dart index f9a9e997bd79..86b9f9597eea 100644 --- a/app/lib/cubits/editor_session.dart +++ b/app/lib/cubits/editor_session.dart @@ -81,6 +81,7 @@ class EditorSessionCubit extends Cubit { _transformCubit.state.pixelRatio, Offset(state.camera.positionX, state.camera.positionY), state.camera.zoom, + state.camera.rotation, ); int resolveToolIndex(DocumentInfo info) { @@ -107,6 +108,7 @@ class EditorSessionCubit extends Cubit { positionX: transform.position.dx, positionY: transform.position.dy, zoom: transform.size, + rotation: transform.rotation, ); if (state.camera == camera) return; emit(state.copyWith(camera: camera)); diff --git a/app/lib/cubits/transform.dart b/app/lib/cubits/transform.dart index be23ff8c4860..334bbdd4a4d4 100644 --- a/app/lib/cubits/transform.dart +++ b/app/lib/cubits/transform.dart @@ -41,30 +41,70 @@ sealed class CameraTransform with _$CameraTransform { @Default(1) double pixelRatio, @Default(Offset.zero) Offset position, @Default(1) double size, + @Default(0) double rotation, FrictionState? friction, ]) = _CameraTransform; CameraTransform withPosition(Offset position) => - CameraTransform(pixelRatio, position, size); + CameraTransform(pixelRatio, position, size, rotation); CameraTransform withPointPosition(Point position) => - CameraTransform(pixelRatio, position.toOffset(), size); + CameraTransform(pixelRatio, position.toOffset(), size, rotation); CameraTransform withSize(double size, [Offset cursor = Offset.zero]) { // Set size and focus on cursor if provided final double newSize = size.clamp(kMinZoom, kMaxZoom); - var mx = localToGlobal(cursor); - mx = (mx - position) * newSize; - return CameraTransform( pixelRatio, - position + (mx - cursor) / newSize, + localToGlobal(cursor) - cursor.rotate(Offset.zero, -rotation) / newSize, newSize, + rotation, + ); + } + + CameraTransform withRotation(double rotation, [Offset cursor = Offset.zero]) { + final normalized = (rotation + pi) % (2 * pi) - pi; + return CameraTransform( + pixelRatio, + localToGlobal(cursor) - cursor.rotate(Offset.zero, -normalized) / size, + size, + normalized, + ); + } + + Offset localToGlobal(Offset local) => + local.rotate(Offset.zero, -rotation) / size + position; + + Offset globalToLocal(Offset global) => + ((global - position) * size).rotate(Offset.zero, rotation); + + Offset localToGlobalDelta(Offset delta) => + delta.rotate(Offset.zero, -rotation) / size; + + Rect localToGlobalRect(Rect rect) => _transformRect(rect, localToGlobal); + + Rect globalToLocalRect(Rect rect) => _transformRect(rect, globalToLocal); + + Rect _transformRect(Rect rect, Offset Function(Offset) transform) { + final points = [ + transform(rect.topLeft), + transform(rect.topRight), + transform(rect.bottomLeft), + transform(rect.bottomRight), + ]; + return Rect.fromLTRB( + points.map((point) => point.dx).reduce(min), + points.map((point) => point.dy).reduce(min), + points.map((point) => point.dx).reduce(max), + points.map((point) => point.dy).reduce(max), ); } - Offset localToGlobal(Offset local) => local / size + position; - Offset globalToLocal(Offset global) => (global - position) * size; + void applyToCanvas(Canvas canvas) { + canvas.rotate(rotation); + canvas.scale(size); + canvas.translate(-position.dx, -position.dy); + } double _getFinalTime( double velocity, @@ -125,6 +165,7 @@ sealed class CameraTransform with _$CameraTransform { pixelRatio, finalPosition, finalScale, + rotation, frictionState, ); } @@ -134,12 +175,13 @@ sealed class CameraTransform with _$CameraTransform { pixelRatio, this.position - position, this.size - size, + rotation, null, ); } CameraTransform improve(RenderResolution resolution, Rect rect) { - return CameraTransform(pixelRatio, rect.topLeft, size, friction); + return CameraTransform(pixelRatio, rect.topLeft, size, 0, friction); } } @@ -149,14 +191,21 @@ class TransformCubit extends Cubit { void move(Offset delta) => emit(state.withPosition(state.position + delta)); - void teleport(Offset position, [double? scale]) => - emit(state.withPosition(position).withSize(scale ?? state.size)); + void teleport(Offset position, [double? scale, double? rotation]) => emit( + state + .withPosition(position) + .withSize(scale ?? state.size) + .withRotation(rotation ?? state.rotation), + ); void zoom(double delta, [Offset cursor = Offset.zero]) => emit(state.withSize(state.size * delta, cursor)); void focus(Offset cursor) => emit(state.withSize(state.size, cursor)); + void rotate(double delta, [Offset cursor = Offset.zero]) => + emit(state.withRotation(state.rotation + delta, cursor)); + void reset() => emit(CameraTransform(state.pixelRatio)); void size(double size, [Offset cursor = Offset.zero]) => @@ -519,6 +568,18 @@ class TransformCubit extends Cubit { teleport(clamped.position, clamped.size); } + void rotateConstrained( + double delta, { + required EditorRuntimeContext runtime, + Offset cursor = Offset.zero, + bool force = false, + }) { + if (delta == 0 || (runtime.viewCubit.state.locks.lockRotation && !force)) { + return; + } + rotate(delta, cursor); + } + void sizeConstrained( double size, { required EditorRuntimeContext runtime, diff --git a/app/lib/cubits/transform.freezed.dart b/app/lib/cubits/transform.freezed.dart index d8f0184bd093..3178168b4101 100644 --- a/app/lib/cubits/transform.freezed.dart +++ b/app/lib/cubits/transform.freezed.dart @@ -163,7 +163,7 @@ as double, /// @nodoc mixin _$CameraTransform implements DiagnosticableTreeMixin { - double get pixelRatio; Offset get position; double get size; FrictionState? get friction; + double get pixelRatio; Offset get position; double get size; double get rotation; FrictionState? get friction; /// Create a copy of CameraTransform /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -175,21 +175,21 @@ $CameraTransformCopyWith get copyWith => _$CameraTransformCopyW void debugFillProperties(DiagnosticPropertiesBuilder properties) { properties ..add(DiagnosticsProperty('type', 'CameraTransform')) - ..add(DiagnosticsProperty('pixelRatio', pixelRatio))..add(DiagnosticsProperty('position', position))..add(DiagnosticsProperty('size', size))..add(DiagnosticsProperty('friction', friction)); + ..add(DiagnosticsProperty('pixelRatio', pixelRatio))..add(DiagnosticsProperty('position', position))..add(DiagnosticsProperty('size', size))..add(DiagnosticsProperty('rotation', rotation))..add(DiagnosticsProperty('friction', friction)); } @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is CameraTransform&&(identical(other.pixelRatio, pixelRatio) || other.pixelRatio == pixelRatio)&&(identical(other.position, position) || other.position == position)&&(identical(other.size, size) || other.size == size)&&(identical(other.friction, friction) || other.friction == friction)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is CameraTransform&&(identical(other.pixelRatio, pixelRatio) || other.pixelRatio == pixelRatio)&&(identical(other.position, position) || other.position == position)&&(identical(other.size, size) || other.size == size)&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.friction, friction) || other.friction == friction)); } @override -int get hashCode => Object.hash(runtimeType,pixelRatio,position,size,friction); +int get hashCode => Object.hash(runtimeType,pixelRatio,position,size,rotation,friction); @override String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'CameraTransform(pixelRatio: $pixelRatio, position: $position, size: $size, friction: $friction)'; + return 'CameraTransform(pixelRatio: $pixelRatio, position: $position, size: $size, rotation: $rotation, friction: $friction)'; } @@ -200,7 +200,7 @@ abstract mixin class $CameraTransformCopyWith<$Res> { factory $CameraTransformCopyWith(CameraTransform value, $Res Function(CameraTransform) _then) = _$CameraTransformCopyWithImpl; @useResult $Res call({ - double pixelRatio, Offset position, double size, FrictionState? friction + double pixelRatio, Offset position, double size, double rotation, FrictionState? friction }); @@ -217,11 +217,12 @@ class _$CameraTransformCopyWithImpl<$Res> /// Create a copy of CameraTransform /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? pixelRatio = null,Object? position = null,Object? size = null,Object? friction = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? pixelRatio = null,Object? position = null,Object? size = null,Object? rotation = null,Object? friction = freezed,}) { return _then(_self.copyWith( pixelRatio: null == pixelRatio ? _self.pixelRatio : pixelRatio // ignore: cast_nullable_to_non_nullable as double,position: null == position ? _self.position : position // ignore: cast_nullable_to_non_nullable as Offset,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable +as double,rotation: null == rotation ? _self.rotation : rotation // ignore: cast_nullable_to_non_nullable as double,friction: freezed == friction ? _self.friction : friction // ignore: cast_nullable_to_non_nullable as FrictionState?, )); @@ -247,12 +248,13 @@ $FrictionStateCopyWith<$Res>? get friction { class _CameraTransform extends CameraTransform with DiagnosticableTreeMixin { - const _CameraTransform([this.pixelRatio = 1, this.position = Offset.zero, this.size = 1, this.friction]): super._(); + const _CameraTransform([this.pixelRatio = 1, this.position = Offset.zero, this.size = 1, this.rotation = 0, this.friction]): super._(); @override@JsonKey() final double pixelRatio; @override@JsonKey() final Offset position; @override@JsonKey() final double size; +@override@JsonKey() final double rotation; @override final FrictionState? friction; /// Create a copy of CameraTransform @@ -266,21 +268,21 @@ _$CameraTransformCopyWith<_CameraTransform> get copyWith => __$CameraTransformCo void debugFillProperties(DiagnosticPropertiesBuilder properties) { properties ..add(DiagnosticsProperty('type', 'CameraTransform')) - ..add(DiagnosticsProperty('pixelRatio', pixelRatio))..add(DiagnosticsProperty('position', position))..add(DiagnosticsProperty('size', size))..add(DiagnosticsProperty('friction', friction)); + ..add(DiagnosticsProperty('pixelRatio', pixelRatio))..add(DiagnosticsProperty('position', position))..add(DiagnosticsProperty('size', size))..add(DiagnosticsProperty('rotation', rotation))..add(DiagnosticsProperty('friction', friction)); } @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _CameraTransform&&(identical(other.pixelRatio, pixelRatio) || other.pixelRatio == pixelRatio)&&(identical(other.position, position) || other.position == position)&&(identical(other.size, size) || other.size == size)&&(identical(other.friction, friction) || other.friction == friction)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _CameraTransform&&(identical(other.pixelRatio, pixelRatio) || other.pixelRatio == pixelRatio)&&(identical(other.position, position) || other.position == position)&&(identical(other.size, size) || other.size == size)&&(identical(other.rotation, rotation) || other.rotation == rotation)&&(identical(other.friction, friction) || other.friction == friction)); } @override -int get hashCode => Object.hash(runtimeType,pixelRatio,position,size,friction); +int get hashCode => Object.hash(runtimeType,pixelRatio,position,size,rotation,friction); @override String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'CameraTransform(pixelRatio: $pixelRatio, position: $position, size: $size, friction: $friction)'; + return 'CameraTransform(pixelRatio: $pixelRatio, position: $position, size: $size, rotation: $rotation, friction: $friction)'; } @@ -291,7 +293,7 @@ abstract mixin class _$CameraTransformCopyWith<$Res> implements $CameraTransform factory _$CameraTransformCopyWith(_CameraTransform value, $Res Function(_CameraTransform) _then) = __$CameraTransformCopyWithImpl; @override @useResult $Res call({ - double pixelRatio, Offset position, double size, FrictionState? friction + double pixelRatio, Offset position, double size, double rotation, FrictionState? friction }); @@ -308,11 +310,12 @@ class __$CameraTransformCopyWithImpl<$Res> /// Create a copy of CameraTransform /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? pixelRatio = null,Object? position = null,Object? size = null,Object? friction = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? pixelRatio = null,Object? position = null,Object? size = null,Object? rotation = null,Object? friction = freezed,}) { return _then(_CameraTransform( null == pixelRatio ? _self.pixelRatio : pixelRatio // ignore: cast_nullable_to_non_nullable as double,null == position ? _self.position : position // ignore: cast_nullable_to_non_nullable as Offset,null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable +as double,null == rotation ? _self.rotation : rotation // ignore: cast_nullable_to_non_nullable as double,freezed == friction ? _self.friction : friction // ignore: cast_nullable_to_non_nullable as FrictionState?, )); diff --git a/app/lib/models/persisted_document_state.dart b/app/lib/models/persisted_document_state.dart index 736d09244be5..4190b1b018ad 100644 --- a/app/lib/models/persisted_document_state.dart +++ b/app/lib/models/persisted_document_state.dart @@ -55,6 +55,7 @@ sealed class PersistedCameraState with _$PersistedCameraState { @Default(0) double positionX, @Default(0) double positionY, @Default(1) double zoom, + @Default(0) double rotation, }) = _PersistedCameraState; factory PersistedCameraState.fromJson(Map json) => @@ -71,6 +72,7 @@ sealed class PersistentLockState with _$PersistentLockState { @Default(false) bool lockZoom, @Default(false) bool lockHorizontal, @Default(false) bool lockVertical, + @Default(false) bool lockRotation, }) = _PersistentLockState; factory PersistentLockState.fromJson(Map json) => diff --git a/app/lib/models/persisted_document_state.freezed.dart b/app/lib/models/persisted_document_state.freezed.dart index fdaf16b0b72f..ded04735a230 100644 --- a/app/lib/models/persisted_document_state.freezed.dart +++ b/app/lib/models/persisted_document_state.freezed.dart @@ -152,7 +152,7 @@ as int?, /// @nodoc mixin _$PersistedCameraState { - double get positionX; double get positionY; double get zoom; + double get positionX; double get positionY; double get zoom; double get rotation; /// Create a copy of PersistedCameraState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -165,16 +165,16 @@ $PersistedCameraStateCopyWith get copyWith => _$PersistedC @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is PersistedCameraState&&(identical(other.positionX, positionX) || other.positionX == positionX)&&(identical(other.positionY, positionY) || other.positionY == positionY)&&(identical(other.zoom, zoom) || other.zoom == zoom)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is PersistedCameraState&&(identical(other.positionX, positionX) || other.positionX == positionX)&&(identical(other.positionY, positionY) || other.positionY == positionY)&&(identical(other.zoom, zoom) || other.zoom == zoom)&&(identical(other.rotation, rotation) || other.rotation == rotation)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,positionX,positionY,zoom); +int get hashCode => Object.hash(runtimeType,positionX,positionY,zoom,rotation); @override String toString() { - return 'PersistedCameraState(positionX: $positionX, positionY: $positionY, zoom: $zoom)'; + return 'PersistedCameraState(positionX: $positionX, positionY: $positionY, zoom: $zoom, rotation: $rotation)'; } @@ -185,7 +185,7 @@ abstract mixin class $PersistedCameraStateCopyWith<$Res> { factory $PersistedCameraStateCopyWith(PersistedCameraState value, $Res Function(PersistedCameraState) _then) = _$PersistedCameraStateCopyWithImpl; @useResult $Res call({ - double positionX, double positionY, double zoom + double positionX, double positionY, double zoom, double rotation }); @@ -202,11 +202,12 @@ class _$PersistedCameraStateCopyWithImpl<$Res> /// Create a copy of PersistedCameraState /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? positionX = null,Object? positionY = null,Object? zoom = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? positionX = null,Object? positionY = null,Object? zoom = null,Object? rotation = null,}) { return _then(_self.copyWith( positionX: null == positionX ? _self.positionX : positionX // ignore: cast_nullable_to_non_nullable as double,positionY: null == positionY ? _self.positionY : positionY // ignore: cast_nullable_to_non_nullable as double,zoom: null == zoom ? _self.zoom : zoom // ignore: cast_nullable_to_non_nullable +as double,rotation: null == rotation ? _self.rotation : rotation // ignore: cast_nullable_to_non_nullable as double, )); } @@ -219,12 +220,13 @@ as double, @JsonSerializable() class _PersistedCameraState implements PersistedCameraState { - const _PersistedCameraState({this.positionX = 0, this.positionY = 0, this.zoom = 1}); + const _PersistedCameraState({this.positionX = 0, this.positionY = 0, this.zoom = 1, this.rotation = 0}); factory _PersistedCameraState.fromJson(Map json) => _$PersistedCameraStateFromJson(json); @override@JsonKey() final double positionX; @override@JsonKey() final double positionY; @override@JsonKey() final double zoom; +@override@JsonKey() final double rotation; /// Create a copy of PersistedCameraState /// with the given fields replaced by the non-null parameter values. @@ -239,16 +241,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _PersistedCameraState&&(identical(other.positionX, positionX) || other.positionX == positionX)&&(identical(other.positionY, positionY) || other.positionY == positionY)&&(identical(other.zoom, zoom) || other.zoom == zoom)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _PersistedCameraState&&(identical(other.positionX, positionX) || other.positionX == positionX)&&(identical(other.positionY, positionY) || other.positionY == positionY)&&(identical(other.zoom, zoom) || other.zoom == zoom)&&(identical(other.rotation, rotation) || other.rotation == rotation)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,positionX,positionY,zoom); +int get hashCode => Object.hash(runtimeType,positionX,positionY,zoom,rotation); @override String toString() { - return 'PersistedCameraState(positionX: $positionX, positionY: $positionY, zoom: $zoom)'; + return 'PersistedCameraState(positionX: $positionX, positionY: $positionY, zoom: $zoom, rotation: $rotation)'; } @@ -259,7 +261,7 @@ abstract mixin class _$PersistedCameraStateCopyWith<$Res> implements $PersistedC factory _$PersistedCameraStateCopyWith(_PersistedCameraState value, $Res Function(_PersistedCameraState) _then) = __$PersistedCameraStateCopyWithImpl; @override @useResult $Res call({ - double positionX, double positionY, double zoom + double positionX, double positionY, double zoom, double rotation }); @@ -276,11 +278,12 @@ class __$PersistedCameraStateCopyWithImpl<$Res> /// Create a copy of PersistedCameraState /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? positionX = null,Object? positionY = null,Object? zoom = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? positionX = null,Object? positionY = null,Object? zoom = null,Object? rotation = null,}) { return _then(_PersistedCameraState( positionX: null == positionX ? _self.positionX : positionX // ignore: cast_nullable_to_non_nullable as double,positionY: null == positionY ? _self.positionY : positionY // ignore: cast_nullable_to_non_nullable as double,zoom: null == zoom ? _self.zoom : zoom // ignore: cast_nullable_to_non_nullable +as double,rotation: null == rotation ? _self.rotation : rotation // ignore: cast_nullable_to_non_nullable as double, )); } @@ -292,7 +295,7 @@ as double, /// @nodoc mixin _$PersistentLockState { - bool get lockCollection; bool get lockLayer; bool get lockZoom; bool get lockHorizontal; bool get lockVertical; + bool get lockCollection; bool get lockLayer; bool get lockZoom; bool get lockHorizontal; bool get lockVertical; bool get lockRotation; /// Create a copy of PersistentLockState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -305,16 +308,16 @@ $PersistentLockStateCopyWith get copyWith => _$PersistentLo @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is PersistentLockState&&(identical(other.lockCollection, lockCollection) || other.lockCollection == lockCollection)&&(identical(other.lockLayer, lockLayer) || other.lockLayer == lockLayer)&&(identical(other.lockZoom, lockZoom) || other.lockZoom == lockZoom)&&(identical(other.lockHorizontal, lockHorizontal) || other.lockHorizontal == lockHorizontal)&&(identical(other.lockVertical, lockVertical) || other.lockVertical == lockVertical)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is PersistentLockState&&(identical(other.lockCollection, lockCollection) || other.lockCollection == lockCollection)&&(identical(other.lockLayer, lockLayer) || other.lockLayer == lockLayer)&&(identical(other.lockZoom, lockZoom) || other.lockZoom == lockZoom)&&(identical(other.lockHorizontal, lockHorizontal) || other.lockHorizontal == lockHorizontal)&&(identical(other.lockVertical, lockVertical) || other.lockVertical == lockVertical)&&(identical(other.lockRotation, lockRotation) || other.lockRotation == lockRotation)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,lockCollection,lockLayer,lockZoom,lockHorizontal,lockVertical); +int get hashCode => Object.hash(runtimeType,lockCollection,lockLayer,lockZoom,lockHorizontal,lockVertical,lockRotation); @override String toString() { - return 'PersistentLockState(lockCollection: $lockCollection, lockLayer: $lockLayer, lockZoom: $lockZoom, lockHorizontal: $lockHorizontal, lockVertical: $lockVertical)'; + return 'PersistentLockState(lockCollection: $lockCollection, lockLayer: $lockLayer, lockZoom: $lockZoom, lockHorizontal: $lockHorizontal, lockVertical: $lockVertical, lockRotation: $lockRotation)'; } @@ -325,7 +328,7 @@ abstract mixin class $PersistentLockStateCopyWith<$Res> { factory $PersistentLockStateCopyWith(PersistentLockState value, $Res Function(PersistentLockState) _then) = _$PersistentLockStateCopyWithImpl; @useResult $Res call({ - bool lockCollection, bool lockLayer, bool lockZoom, bool lockHorizontal, bool lockVertical + bool lockCollection, bool lockLayer, bool lockZoom, bool lockHorizontal, bool lockVertical, bool lockRotation }); @@ -342,13 +345,14 @@ class _$PersistentLockStateCopyWithImpl<$Res> /// Create a copy of PersistentLockState /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? lockCollection = null,Object? lockLayer = null,Object? lockZoom = null,Object? lockHorizontal = null,Object? lockVertical = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? lockCollection = null,Object? lockLayer = null,Object? lockZoom = null,Object? lockHorizontal = null,Object? lockVertical = null,Object? lockRotation = null,}) { return _then(_self.copyWith( lockCollection: null == lockCollection ? _self.lockCollection : lockCollection // ignore: cast_nullable_to_non_nullable as bool,lockLayer: null == lockLayer ? _self.lockLayer : lockLayer // ignore: cast_nullable_to_non_nullable as bool,lockZoom: null == lockZoom ? _self.lockZoom : lockZoom // ignore: cast_nullable_to_non_nullable as bool,lockHorizontal: null == lockHorizontal ? _self.lockHorizontal : lockHorizontal // ignore: cast_nullable_to_non_nullable as bool,lockVertical: null == lockVertical ? _self.lockVertical : lockVertical // ignore: cast_nullable_to_non_nullable +as bool,lockRotation: null == lockRotation ? _self.lockRotation : lockRotation // ignore: cast_nullable_to_non_nullable as bool, )); } @@ -361,7 +365,7 @@ as bool, @JsonSerializable() class _PersistentLockState extends PersistentLockState { - const _PersistentLockState({this.lockCollection = false, this.lockLayer = false, this.lockZoom = false, this.lockHorizontal = false, this.lockVertical = false}): super._(); + const _PersistentLockState({this.lockCollection = false, this.lockLayer = false, this.lockZoom = false, this.lockHorizontal = false, this.lockVertical = false, this.lockRotation = false}): super._(); factory _PersistentLockState.fromJson(Map json) => _$PersistentLockStateFromJson(json); @override@JsonKey() final bool lockCollection; @@ -369,6 +373,7 @@ class _PersistentLockState extends PersistentLockState { @override@JsonKey() final bool lockZoom; @override@JsonKey() final bool lockHorizontal; @override@JsonKey() final bool lockVertical; +@override@JsonKey() final bool lockRotation; /// Create a copy of PersistentLockState /// with the given fields replaced by the non-null parameter values. @@ -383,16 +388,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _PersistentLockState&&(identical(other.lockCollection, lockCollection) || other.lockCollection == lockCollection)&&(identical(other.lockLayer, lockLayer) || other.lockLayer == lockLayer)&&(identical(other.lockZoom, lockZoom) || other.lockZoom == lockZoom)&&(identical(other.lockHorizontal, lockHorizontal) || other.lockHorizontal == lockHorizontal)&&(identical(other.lockVertical, lockVertical) || other.lockVertical == lockVertical)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _PersistentLockState&&(identical(other.lockCollection, lockCollection) || other.lockCollection == lockCollection)&&(identical(other.lockLayer, lockLayer) || other.lockLayer == lockLayer)&&(identical(other.lockZoom, lockZoom) || other.lockZoom == lockZoom)&&(identical(other.lockHorizontal, lockHorizontal) || other.lockHorizontal == lockHorizontal)&&(identical(other.lockVertical, lockVertical) || other.lockVertical == lockVertical)&&(identical(other.lockRotation, lockRotation) || other.lockRotation == lockRotation)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,lockCollection,lockLayer,lockZoom,lockHorizontal,lockVertical); +int get hashCode => Object.hash(runtimeType,lockCollection,lockLayer,lockZoom,lockHorizontal,lockVertical,lockRotation); @override String toString() { - return 'PersistentLockState(lockCollection: $lockCollection, lockLayer: $lockLayer, lockZoom: $lockZoom, lockHorizontal: $lockHorizontal, lockVertical: $lockVertical)'; + return 'PersistentLockState(lockCollection: $lockCollection, lockLayer: $lockLayer, lockZoom: $lockZoom, lockHorizontal: $lockHorizontal, lockVertical: $lockVertical, lockRotation: $lockRotation)'; } @@ -403,7 +408,7 @@ abstract mixin class _$PersistentLockStateCopyWith<$Res> implements $PersistentL factory _$PersistentLockStateCopyWith(_PersistentLockState value, $Res Function(_PersistentLockState) _then) = __$PersistentLockStateCopyWithImpl; @override @useResult $Res call({ - bool lockCollection, bool lockLayer, bool lockZoom, bool lockHorizontal, bool lockVertical + bool lockCollection, bool lockLayer, bool lockZoom, bool lockHorizontal, bool lockVertical, bool lockRotation }); @@ -420,13 +425,14 @@ class __$PersistentLockStateCopyWithImpl<$Res> /// Create a copy of PersistentLockState /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? lockCollection = null,Object? lockLayer = null,Object? lockZoom = null,Object? lockHorizontal = null,Object? lockVertical = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? lockCollection = null,Object? lockLayer = null,Object? lockZoom = null,Object? lockHorizontal = null,Object? lockVertical = null,Object? lockRotation = null,}) { return _then(_PersistentLockState( lockCollection: null == lockCollection ? _self.lockCollection : lockCollection // ignore: cast_nullable_to_non_nullable as bool,lockLayer: null == lockLayer ? _self.lockLayer : lockLayer // ignore: cast_nullable_to_non_nullable as bool,lockZoom: null == lockZoom ? _self.lockZoom : lockZoom // ignore: cast_nullable_to_non_nullable as bool,lockHorizontal: null == lockHorizontal ? _self.lockHorizontal : lockHorizontal // ignore: cast_nullable_to_non_nullable as bool,lockVertical: null == lockVertical ? _self.lockVertical : lockVertical // ignore: cast_nullable_to_non_nullable +as bool,lockRotation: null == lockRotation ? _self.lockRotation : lockRotation // ignore: cast_nullable_to_non_nullable as bool, )); } diff --git a/app/lib/models/persisted_document_state.g.dart b/app/lib/models/persisted_document_state.g.dart index e27aa3c29b0f..0dd140bdef71 100644 --- a/app/lib/models/persisted_document_state.g.dart +++ b/app/lib/models/persisted_document_state.g.dart @@ -24,6 +24,7 @@ _PersistedCameraState _$PersistedCameraStateFromJson(Map json) => positionX: (json['positionX'] as num?)?.toDouble() ?? 0, positionY: (json['positionY'] as num?)?.toDouble() ?? 0, zoom: (json['zoom'] as num?)?.toDouble() ?? 1, + rotation: (json['rotation'] as num?)?.toDouble() ?? 0, ); Map _$PersistedCameraStateToJson( @@ -32,6 +33,7 @@ Map _$PersistedCameraStateToJson( 'positionX': instance.positionX, 'positionY': instance.positionY, 'zoom': instance.zoom, + 'rotation': instance.rotation, }; _PersistentLockState _$PersistentLockStateFromJson(Map json) => @@ -41,6 +43,7 @@ _PersistentLockState _$PersistentLockStateFromJson(Map json) => lockZoom: json['lockZoom'] as bool? ?? false, lockHorizontal: json['lockHorizontal'] as bool? ?? false, lockVertical: json['lockVertical'] as bool? ?? false, + lockRotation: json['lockRotation'] as bool? ?? false, ); Map _$PersistentLockStateToJson( @@ -51,6 +54,7 @@ Map _$PersistentLockStateToJson( 'lockZoom': instance.lockZoom, 'lockHorizontal': instance.lockHorizontal, 'lockVertical': instance.lockVertical, + 'lockRotation': instance.lockRotation, }; _PersistedNavigatorState _$PersistedNavigatorStateFromJson(Map json) => diff --git a/app/lib/renderers/backgrounds/image.dart b/app/lib/renderers/backgrounds/image.dart index 2e3bb19771a1..8b0d852dd528 100644 --- a/app/lib/renderers/backgrounds/image.dart +++ b/app/lib/renderers/backgrounds/image.dart @@ -17,21 +17,17 @@ class ImageBackgroundRenderer extends Renderer { bool foreground = false, ]) { if (image == null) return; - final sizeX = element.width * element.scaleX * transform.size; - final sizeY = element.height * element.scaleY * transform.size; - var offsetX = (transform.position.dx * -transform.size) % sizeX; - if (offsetX > 0) { - offsetX -= sizeX; - } - var offsetY = (transform.position.dy * -transform.size) % sizeY; - if (offsetY > 0) { - offsetY -= sizeY; - } + final sizeX = element.width * element.scaleX; + final sizeY = element.height * element.scaleY; + if (sizeX <= 0 || sizeY <= 0) return; + final viewport = transform.localToGlobalRect(Offset.zero & size); + final offsetX = (viewport.left / sizeX).floor() * sizeX; + final offsetY = (viewport.top / sizeY).floor() * sizeY; var paint = Paint(); - for (var x = offsetX - sizeX; x < size.width + sizeX; x += sizeX) { - for (var y = offsetY - sizeY; y < size.height + sizeY; y += sizeY) { + for (var x = offsetX; x < viewport.right; x += sizeX) { + for (var y = offsetY; y < viewport.bottom; y += sizeY) { canvas.drawImageRect( image!, Rect.fromLTWH( diff --git a/app/lib/renderers/backgrounds/svg.dart b/app/lib/renderers/backgrounds/svg.dart index 5d34bf21e382..fcd20c9700fb 100644 --- a/app/lib/renderers/backgrounds/svg.dart +++ b/app/lib/renderers/backgrounds/svg.dart @@ -36,24 +36,17 @@ class SvgBackgroundRenderer extends Renderer { bool foreground = false, ]) { if (_pictureInfo == null) return; - final sizeX = element.width * element.scaleX * transform.size; - final sizeY = element.height * element.scaleY * transform.size; - var offsetX = (transform.position.dx * -transform.size) % sizeX; - if (offsetX > 0) { - offsetX -= sizeX; - } - var offsetY = (transform.position.dy * -transform.size) % sizeY; - if (offsetY > 0) { - offsetY -= sizeY; - } - for (var x = offsetX - sizeX; x < size.width + sizeX; x += sizeX) { - for (var y = offsetY - sizeY; y < size.height + sizeY; y += sizeY) { + final sizeX = element.width * element.scaleX; + final sizeY = element.height * element.scaleY; + if (sizeX <= 0 || sizeY <= 0) return; + final viewport = transform.localToGlobalRect(Offset.zero & size); + final offsetX = (viewport.left / sizeX).floor() * sizeX; + final offsetY = (viewport.top / sizeY).floor() * sizeY; + for (var x = offsetX; x < viewport.right; x += sizeX) { + for (var y = offsetY; y < viewport.bottom; y += sizeY) { canvas.save(); canvas.translate(x, y); - canvas.scale( - element.scaleX * transform.size, - element.scaleY * transform.size, - ); + canvas.scale(element.scaleX, element.scaleY); canvas.drawPicture(_pictureInfo!.picture); canvas.restore(); } diff --git a/app/lib/renderers/backgrounds/texture.dart b/app/lib/renderers/backgrounds/texture.dart index ab9b3fb658f3..66727b867ff7 100644 --- a/app/lib/renderers/backgrounds/texture.dart +++ b/app/lib/renderers/backgrounds/texture.dart @@ -16,26 +16,17 @@ class TextureBackgroundRenderer extends Renderer { ColorScheme? colorScheme, bool foreground = false, ]) { - // Overshoot bounds to avoid 1px fractional bleeding - final overshoot = 20.0; - final oversize = Size( - size.width + overshoot * 2, - size.height + overshoot * 2, + final viewport = transform.localToGlobalRect( + (Offset.zero & size).inflate(20), ); - // We must shift the offset that drawSurfaceTextureOnCanvas uses - // to keep the pattern visually anchored accurately despite our expanded canvas - final shiftedOffset = - transform.position - - Offset(overshoot / transform.size, overshoot / transform.size); - drawSurfaceTextureOnCanvas( texture, canvas, - transform.size, - shiftedOffset, - oversize, - Offset(-overshoot, -overshoot), + 1, + viewport.topLeft, + viewport.size, + viewport.topLeft, true, ); } diff --git a/app/lib/selections/document.dart b/app/lib/selections/document.dart index 6ef876aefb34..0e80f65e86bf 100644 --- a/app/lib/selections/document.dart +++ b/app/lib/selections/document.dart @@ -412,6 +412,29 @@ class _UtilitiesViewState extends State<_UtilitiesView> context.read().bake(); }, ), + ExactSlider( + header: Text(AppLocalizations.of(context).rotation), + value: + context.read().state.rotation * 180 / pi, + defaultValue: 0, + min: -180, + max: 180, + fractionDigits: 0, + onChanged: (value) { + final editorController = context.read(); + final size = editorController + .rendererCubit + .state + .cameraViewport + .toSize(); + final transform = editorController.transformCubit; + transform.rotateConstrained( + value * pi / 180 - transform.state.rotation, + cursor: size.center(Offset.zero), + runtime: editorController, + ); + }, + ), ], ), ][_tabController.index], diff --git a/app/lib/view_painter.dart b/app/lib/view_painter.dart index 31d5d59654c9..6b7ea8f98f68 100644 --- a/app/lib/view_painter.dart +++ b/app/lib/view_painter.dart @@ -171,8 +171,7 @@ class ForegroundPainter extends CustomPainter { void paint(Canvas canvas, Size size) { final sel = selection; if (renderers.isEmpty && sel == null) return; - canvas.scale(transform.size); - canvas.translate(-transform.position.dx, -transform.position.dy); + transform.applyToCanvas(canvas); _paintRenderers( canvas, size, @@ -193,13 +192,12 @@ class ForegroundPainter extends CustomPainter { final rect = selection.expandedRect; if (rect == null) return; // Don't allow drawing outside the bounds of the viewport. - var bounds = - transform.position & - ((Size(size.width - kNavigationRailWidth, size.height)) / - transform.size); + var viewport = + Offset.zero & Size(size.width - kNavigationRailWidth, size.height); if (navigatorPosition == NavigatorPosition.left) { - bounds = bounds.translate(kNavigationRailWidth / transform.size, 0); + viewport = viewport.translate(kNavigationRailWidth, 0); } + final bounds = transform.localToGlobalRect(viewport); final intersection = rect.intersect(bounds); if (intersection.isEmpty) return; canvas.drawRRect( @@ -246,25 +244,18 @@ class ViewPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - var areaRect = currentArea?.rect; - if (areaRect != null) { - areaRect = Rect.fromPoints( - transform.globalToLocal(areaRect.topLeft), - transform.globalToLocal(areaRect.bottomRight), - ); - } if (renderBackground) { canvas.drawColor(Colors.white, BlendMode.src); + } + canvas.save(); + transform.applyToCanvas(canvas); + if (renderBackground) { for (final e in cameraViewport.backgrounds) { e.build(canvas, size, document, page, info, transform, colorScheme); } } final belowLayerImage = cameraViewport.belowLayerImage; final bakedRect = cameraViewport.toRect(); - final bakedDst = Rect.fromPoints( - transform.globalToLocal(bakedRect.topLeft), - transform.globalToLocal(bakedRect.bottomRight), - ); if (renderBakedLayers && belowLayerImage != null) { canvas.drawImageRect( belowLayerImage, @@ -273,14 +264,14 @@ class ViewPainter extends CustomPainter { belowLayerImage.width.toDouble(), belowLayerImage.height.toDouble(), ), - bakedDst, + bakedRect, Paint(), ); } - final areaSelectionWidth = 5 * transform.size; + final areaRect = currentArea?.rect; + final areaSelectionWidth = 5 / transform.size; if (areaRect != null) { - final visibleRect = - transform.position & (Size(size.width, size.height) / transform.size); + final visibleRect = transform.localToGlobalRect(Offset.zero & size); final currentAreaColor = currentArea?.color?.toColor(); final paint = Paint() ..style = PaintingStyle.stroke @@ -290,17 +281,12 @@ class ViewPainter extends CustomPainter { for (final area in page.areas) { if (area == currentArea || !area.rect.overlaps(visibleRect)) continue; if (areaRect.overlaps(area.rect)) continue; - var rect = area.rect; - rect = Rect.fromPoints( - transform.globalToLocal(rect.topLeft), - transform.globalToLocal(rect.bottomRight), - ); final areaPaint = Paint() ..style = PaintingStyle.stroke ..color = area.color?.toColor() ?? colorScheme?.secondary ?? Colors.grey ..strokeWidth = areaSelectionWidth; - canvas.drawRect(rect.inflate(areaSelectionWidth / 2), areaPaint); + canvas.drawRect(area.rect.inflate(areaSelectionWidth / 2), areaPaint); } canvas.clipRect(areaRect); } @@ -314,14 +300,11 @@ class ViewPainter extends CustomPainter { canvas.drawImageRect( image, Offset.zero & Size(image.width.toDouble(), image.height.toDouble()), - bakedDst, + bakedRect, Paint(), ); } catch (_) {} } - canvas.scale(transform.size, transform.size); - canvas.translate(-transform.position.dx, -transform.position.dy); - final renderers = cameraViewport.visibleUnbakedElements.where((renderer) { final state = cameraViewport.rendererStates[renderer.id]; return !(invisibleLayers?.contains(renderer.layer) ?? false) && @@ -337,8 +320,6 @@ class ViewPainter extends CustomPainter { colorScheme, renderers, ); - canvas.translate(transform.position.dx, transform.position.dy); - canvas.scale(1 / transform.size, 1 / transform.size); final aboveLayerImage = cameraViewport.aboveLayerImage; if (renderBakedLayers && aboveLayerImage != null) { canvas.drawImageRect( @@ -348,10 +329,11 @@ class ViewPainter extends CustomPainter { aboveLayerImage.width.toDouble(), aboveLayerImage.height.toDouble(), ), - bakedDst, + bakedRect, Paint(), ); } + canvas.restore(); } @override diff --git a/app/lib/views/edit.dart b/app/lib/views/edit.dart index b3de5aeb319d..a2a734ac8364 100644 --- a/app/lib/views/edit.dart +++ b/app/lib/views/edit.dart @@ -484,6 +484,14 @@ class _EditToolbarState extends State { PhosphorIconsLight.magnifyingGlassPlus, AppLocalizations.of(context).zoom, ), + buildButton( + locks.lockRotation, + () => locks.copyWith( + lockRotation: !locks.lockRotation, + ), + PhosphorIconsLight.arrowClockwise, + AppLocalizations.of(context).rotation, + ), buildButton( locks.lockHorizontal, () => locks.copyWith( diff --git a/app/lib/views/main.dart b/app/lib/views/main.dart index 1925095a591d..9c1f4f03a6d4 100644 --- a/app/lib/views/main.dart +++ b/app/lib/views/main.dart @@ -392,6 +392,7 @@ class _ProjectPageState extends State { initialSession.camera.positionY, ), initialSession.camera.zoom, + initialSession.camera.rotation, ); final editorSessionCubit = EditorSessionCubit( repository: documentStateRepository, diff --git a/app/lib/views/view.dart b/app/lib/views/view.dart index 2e8f1004a407..0834f304b62c 100644 --- a/app/lib/views/view.dart +++ b/app/lib/views/view.dart @@ -43,6 +43,7 @@ class _MainViewViewportState extends State with WidgetsBindingObserver, SingleTickerProviderStateMixin { late final AnimationController _animationController; double size = 1.0; + double gestureRotation = 0; GlobalKey paintKey = GlobalKey(); _MouseState _mouseState = _MouseState.normal; bool _isShiftPressed = false, _isAltPressed = false, _isCtrlPressed = false; @@ -342,7 +343,7 @@ class _MainViewViewportState extends State if (event.pointer == inputState.pointers.first) { final transform = context.read().state; cubit.transformCubit.moveConstrained( - -event.delta / transform.size, + transform.localToGlobalDelta(-event.delta), runtime: cubit, bloc: context.read(), currentArea: state.currentArea, @@ -655,11 +656,21 @@ class _MainViewViewportState extends State .read() .state .gestureSensitivity; + final rotationDelta = + details.rotation - gestureRotation; + gestureRotation = details.rotation; + cubit.transformCubit.rotateConstrained( + rotationDelta / sensitivity, + cursor: details.localFocalPoint, + runtime: cubit, + ); if (details.scale == 1) { cubit.transformCubit.moveConstrained( - -details.focalPointDelta / - sensitivity / - cubit.transformCubit.state.size, + cubit.transformCubit.state + .localToGlobalDelta( + -details.focalPointDelta, + ) / + sensitivity, runtime: cubit, bloc: bloc, currentArea: state.currentArea, @@ -703,9 +714,13 @@ class _MainViewViewportState extends State cubit.rendererCubit .cancelDelayedBake(); cubit.transformCubit.slideConstrained( - details.velocity.pixelsPerSecond / - sensitivity / - cubit.transformCubit.state.size, + cubit.transformCubit.state + .localToGlobalDelta( + details + .velocity + .pixelsPerSecond, + ) / + sensitivity, details.scaleVelocity, runtime: cubit, currentArea: state.currentArea, @@ -782,6 +797,7 @@ class _MainViewViewportState extends State } point = details.localFocalPoint; size = 1; + gestureRotation = 0; }, onLongPressStart: (details) => getHandler().onLongPressStart( @@ -835,12 +851,14 @@ class _MainViewViewportState extends State } else { cubit.transformCubit .moveConstrained( - (_mouseState == - _MouseState - .inverse - ? Offset(dy, dx) - : Offset(dx, dy)) / - transform.size, + transform + .localToGlobalDelta( + _mouseState == + _MouseState + .inverse + ? Offset(dy, dx) + : Offset(dx, dy), + ), runtime: cubit, bloc: bloc, currentArea: diff --git a/app/test/cubits/editor_session_test.dart b/app/test/cubits/editor_session_test.dart index 70e688220d11..6b2ef4de8d32 100644 --- a/app/test/cubits/editor_session_test.dart +++ b/app/test/cubits/editor_session_test.dart @@ -27,8 +27,9 @@ void main() { positionX: 10, positionY: 20, zoom: 2, + rotation: 0.5, ), - locks: const PersistentLockState(lockZoom: true), + locks: const PersistentLockState(lockZoom: true, lockRotation: true), selectedTool: const PersistedToolSelection( toolId: 'tool-a', toolIndex: 3, @@ -58,6 +59,7 @@ void main() { expect(state.version, kPersistedDocumentStateVersion); expect(state.camera.zoom, 1); + expect(state.camera.rotation, 0); expect(state.locks, const PersistentLockState()); expect(state.navigator.page, NavigatorPage.waypoints.name); }); @@ -340,7 +342,7 @@ void main() { contentHash: 'hash-a', ); - transformCubit.teleport(const Offset(10, 20), 2); + transformCubit.teleport(const Offset(10, 20), 2, 0.5); await Future.delayed(const Duration(milliseconds: 300)); expect(await fileSystem.getFile('path/a'), isNull); @@ -350,6 +352,7 @@ void main() { expect(saved?.camera.positionX, 10); expect(saved?.camera.positionY, 20); expect(saved?.camera.zoom, 2); + expect(saved?.camera.rotation, 0.5); await cubit.close(); await transformCubit.close(); diff --git a/app/test/cubits/transform_test.dart b/app/test/cubits/transform_test.dart new file mode 100644 index 000000000000..aaf7fa5ac88f --- /dev/null +++ b/app/test/cubits/transform_test.dart @@ -0,0 +1,64 @@ +import 'dart:math'; + +import 'package:butterfly/cubits/transform.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('CameraTransform rotation', () { + test('converts between screen and document coordinates', () { + const transform = CameraTransform(1, Offset(10, 20), 2, pi / 3); + const documentPoint = Offset(35, -12); + + final screenPoint = transform.globalToLocal(documentPoint); + final restored = transform.localToGlobal(screenPoint); + + expect(restored.dx, closeTo(documentPoint.dx, 1e-9)); + expect(restored.dy, closeTo(documentPoint.dy, 1e-9)); + }); + + test('keeps the gesture focal point fixed while rotating', () { + const transform = CameraTransform(1, Offset(10, 20), 2, pi / 6); + const focalPoint = Offset(240, 120); + final documentPoint = transform.localToGlobal(focalPoint); + + final rotated = transform.withRotation(pi / 2, focalPoint); + + final restored = rotated.localToGlobal(focalPoint); + expect(restored.dx, closeTo(documentPoint.dx, 1e-9)); + expect(restored.dy, closeTo(documentPoint.dy, 1e-9)); + }); + + test('keeps the gesture focal point fixed while zooming', () { + const transform = CameraTransform(1, Offset(10, 20), 2, pi / 4); + const focalPoint = Offset(240, 120); + final documentPoint = transform.localToGlobal(focalPoint); + + final zoomed = transform.withSize(4, focalPoint); + + final restored = zoomed.localToGlobal(focalPoint); + expect(restored.dx, closeTo(documentPoint.dx, 1e-9)); + expect(restored.dy, closeTo(documentPoint.dy, 1e-9)); + }); + + test('uses all four corners for a rotated viewport', () { + const transform = CameraTransform(1, Offset.zero, 1, pi / 4); + const viewport = Rect.fromLTWH(0, 0, 100, 50); + final bounds = transform.localToGlobalRect(viewport); + final corners = [ + viewport.topLeft, + viewport.topRight, + viewport.bottomLeft, + viewport.bottomRight, + ].map(transform.localToGlobal); + + expect(bounds.left, closeTo(corners.map((e) => e.dx).reduce(min), 1e-9)); + expect(bounds.top, closeTo(corners.map((e) => e.dy).reduce(min), 1e-9)); + expect(bounds.right, closeTo(corners.map((e) => e.dx).reduce(max), 1e-9)); + expect( + bounds.bottom, + closeTo(corners.map((e) => e.dy).reduce(max), 1e-9), + ); + }); + }); +} diff --git a/metadata/en-US/changelogs/188.txt b/metadata/en-US/changelogs/188.txt index 945728b3f85d..3520c099c718 100644 --- a/metadata/en-US/changelogs/188.txt +++ b/metadata/en-US/changelogs/188.txt @@ -1,5 +1,8 @@ * Add persistent document states ([#1077](https://github.com/LinwoodDev/Butterfly/issues/1077)) * Add shear property for elements +* Add camera rotation support ([#977](https://github.com/LinwoodDev/Butterfly/issues/977)) +* Add custom fonts to label ([#1011](https://github.com/LinwoodDev/Butterfly/issues/1011)) +* Add option to customize default file name globally and in template ([#1041](https://github.com/LinwoodDev/Butterfly/issues/1041)) * Reorder top corner menu to have home on top ([#1161](https://github.com/LinwoodDev/Butterfly/issues/1161)) * Rebuild internal settings pages * Add search bar to settings pages ([#1158](https://github.com/LinwoodDev/Butterfly/issues/1158)) From ad55dc88a6ee935175421fb2ed6f8fa226e77670 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Wed, 15 Jul 2026 12:25:18 +0200 Subject: [PATCH 090/117] Further improve large page rendering performance --- app/lib/cubits/editor_renderer.dart | 24 ++++++-- app/lib/cubits/transform.dart | 18 +++--- app/lib/renderers/elements/pen.dart | 69 ++++++++++++----------- app/lib/renderers/renderer.dart | 32 +++++------ app/test/cubits/editor_renderer_test.dart | 57 +++++++++++++++++++ 5 files changed, 134 insertions(+), 66 deletions(-) diff --git a/app/lib/cubits/editor_renderer.dart b/app/lib/cubits/editor_renderer.dart index fcdb1ff9371c..3b42d0a8756f 100644 --- a/app/lib/cubits/editor_renderer.dart +++ b/app/lib/cubits/editor_renderer.dart @@ -17,9 +17,11 @@ sealed class RendererRuntimeState with _$RendererRuntimeState { } class _RendererSpatialIndex { - _RendererSpatialIndex(List> renderers) { + _RendererSpatialIndex(List> renderers) + : _bounds = List.filled(renderers.length, null) { for (var index = 0; index < renderers.length; index++) { final bounds = renderers[index].expandedRect; + _bounds[index] = bounds; if (bounds == null) { _unbounded.add(index); continue; @@ -47,6 +49,10 @@ class _RendererSpatialIndex { final Map<(int, int), List> _cells = {}; final List _large = []; final List _unbounded = []; + final List _bounds; + + bool _isVisible(int index, Rect rect) => + _bounds[index]?.overlaps(rect) ?? true; List> query( List> renderers, @@ -59,7 +65,10 @@ class _RendererSpatialIndex { final bottom = (rect.bottom / _cellSize).floor(); final queryCellCount = (right - left + 1) * (bottom - top + 1); if (queryCellCount > _maxQueryCells) { - return renderers.where((renderer) => renderer.isVisible(rect)).toList(); + return [ + for (var index = 0; index < renderers.length; index++) + if (_isVisible(index, rect)) renderers[index], + ]; } for (var x = left; x <= right; x++) { for (var y = top; y <= bottom; y++) { @@ -69,7 +78,7 @@ class _RendererSpatialIndex { } final ordered = indices.toList()..sort(); return ordered - .where((index) => renderers[index].isVisible(rect)) + .where((index) => _isVisible(index, rect)) .map((index) => renderers[index]) .toList(growable: false); } @@ -93,6 +102,7 @@ class RendererCubit extends Cubit { Timer? _transformDebounceTimer; _RendererSpatialIndex? _spatialIndex; List>? _indexedRenderers; + Set>? _indexedUnbaked; void bindController(EditorController controller) { _controller = controller; @@ -169,6 +179,7 @@ class RendererCubit extends Cubit { void _invalidateSpatialIndex() { _spatialIndex = null; _indexedRenderers = null; + _indexedUnbaked = null; } List> visibleRenderers(Rect rect) { @@ -176,6 +187,7 @@ class RendererCubit extends Cubit { if (all == null) { all = renderers; _indexedRenderers = all; + _indexedUnbaked = state.cameraViewport.unbakedElements.toSet(); _spatialIndex = _RendererSpatialIndex(all); } return _spatialIndex!.query(all, rect); @@ -296,8 +308,7 @@ class RendererCubit extends Cubit { final currentVisibleUnbaked = state.cameraViewport.visibleUnbakedElements; final visible = visibleRenderers(rect); - final unbakedSet = unbaked.toSet(); - final visibleUnbaked = visible.where(unbakedSet.contains).toList(); + final visibleUnbaked = visible.where(_indexedUnbaked!.contains).toList(); if (sameRendererList(visible, currentVisible) && sameRendererList(visibleUnbaked, currentVisibleUnbaked)) { @@ -694,6 +705,7 @@ class RendererCubit extends Cubit { rendererStates: allRendererStates, invisibleLayers: invisibleLayers, ); + rendererCubit._invalidateSpatialIndex(); rendererCubit.setViewport(newViewport); }); @@ -849,7 +861,7 @@ class RendererCubit extends Cubit { }) async { final rendererCubit = this; final transformCubit = controller.transformCubit; - if (unbakedElements != null) rendererCubit._invalidateSpatialIndex(); + rendererCubit._invalidateSpatialIndex(); final elementsToCheck = unbakedElements ?? rendererCubit.renderers; final oldViewport = rendererCubit.state.cameraViewport; final newViewport = oldViewport.unbake( diff --git a/app/lib/cubits/transform.dart b/app/lib/cubits/transform.dart index 334bbdd4a4d4..10c0d09fe9ef 100644 --- a/app/lib/cubits/transform.dart +++ b/app/lib/cubits/transform.dart @@ -86,17 +86,15 @@ sealed class CameraTransform with _$CameraTransform { Rect globalToLocalRect(Rect rect) => _transformRect(rect, globalToLocal); Rect _transformRect(Rect rect, Offset Function(Offset) transform) { - final points = [ - transform(rect.topLeft), - transform(rect.topRight), - transform(rect.bottomLeft), - transform(rect.bottomRight), - ]; + final topLeft = transform(rect.topLeft); + final topRight = transform(rect.topRight); + final bottomLeft = transform(rect.bottomLeft); + final bottomRight = transform(rect.bottomRight); return Rect.fromLTRB( - points.map((point) => point.dx).reduce(min), - points.map((point) => point.dy).reduce(min), - points.map((point) => point.dx).reduce(max), - points.map((point) => point.dy).reduce(max), + min(min(topLeft.dx, topRight.dx), min(bottomLeft.dx, bottomRight.dx)), + min(min(topLeft.dy, topRight.dy), min(bottomLeft.dy, bottomRight.dy)), + max(max(topLeft.dx, topRight.dx), max(bottomLeft.dx, bottomRight.dx)), + max(max(topLeft.dy, topRight.dy), max(bottomLeft.dy, bottomRight.dy)), ); } diff --git a/app/lib/renderers/elements/pen.dart b/app/lib/renderers/elements/pen.dart index 1e3cffa12c07..e8a815e68e65 100644 --- a/app/lib/renderers/elements/pen.dart +++ b/app/lib/renderers/elements/pen.dart @@ -46,7 +46,8 @@ class PenRenderer extends Renderer { _cachedFillPath = Path(); final first = points.first; _cachedFillPath!.moveTo(first.x, first.y); - for (final point in points.sublist(1)) { + for (var i = 1; i < points.length; i++) { + final point = points[i]; _cachedFillPath!.lineTo(point.x, point.y); } } @@ -222,10 +223,10 @@ class PenRenderer extends Renderer { final property = element.property; final center = rect.center; var outlinePoints = freehand.getStroke( - element.points - .map((e) => e.scale(currentZoom, center)) - .map((e) => e.toFreehandPoint()) - .toList(), + [ + for (final point in element.points) + point.scale(currentZoom, center).toFreehandPoint(), + ], options: freehand.StrokeOptions( size: property.strokeWidth * currentZoom, thinning: property.thinning.clamp(0, 1), @@ -235,9 +236,10 @@ class PenRenderer extends Renderer { ), ); - return outlinePoints - .map((e) => e.scaleFromCenter(1 / currentZoom, center)) - .toList(); + return [ + for (final point in outlinePoints) + point.scaleFromCenter(1 / currentZoom, center), + ]; } @override @@ -254,10 +256,13 @@ class PenRenderer extends Renderer { final color = property.paint.previewColor; if (fill.a > 0) { final first = points.first; - var path = 'M ${first.x} ${first.y}'; - points.sublist(1).forEach((point) => path += ' L ${point.x} ${point.y}'); + final path = StringBuffer('M ${first.x} ${first.y}'); + for (var i = 1; i < points.length; i++) { + final point = points[i]; + path.write(' L ${point.x} ${point.y}'); + } xml.getElement('svg')?.createElement('path') - ?..setAttribute('d', path) + ?..setAttribute('d', path.toString()) ..setAttribute('fill', fill.toHexString(alpha: false)) ..setAttribute('fill-opacity', '${fill.a / 255}') ..setAttribute('stroke', 'none') @@ -265,8 +270,6 @@ class PenRenderer extends Renderer { ..setAttribute('stroke-linejoin', 'round'); } if (color.a > 0) { - var path = ''; - // 1. Get the outline points from the input points var outlinePoints = _getOutlinePoints(); @@ -277,14 +280,15 @@ class PenRenderer extends Renderer { } final first = outlinePoints.first; - path += 'M ${first.roundedX()} ${first.roundedY()}'; - for (final point in outlinePoints.sublist(1)) { - path += ' L ${point.roundedX()} ${point.roundedY()}'; + final path = StringBuffer('M ${first.roundedX()} ${first.roundedY()}'); + for (var i = 1; i < outlinePoints.length; i++) { + final point = outlinePoints[i]; + path.write(' L ${point.roundedX()} ${point.roundedY()}'); } - path += ' Z'; + path.write(' Z'); xml.getElement('svg')?.createElement('path') - ?..setAttribute('d', path) + ?..setAttribute('d', path.toString()) ..setAttribute('fill', color.toHexString(alpha: false)) ..setAttribute('fill-opacity', '${color.a / 255}') ..setAttribute('stroke', 'none') @@ -294,20 +298,21 @@ class PenRenderer extends Renderer { } List movePoints(Offset position, double scaleX, double scaleY) { - var topLeft = element.points - .map((e) => e.toOffset()) - .reduce( - (value, element) => - Offset(min(value.dx, element.dx), min(value.dy, element.dy)), - ); - return element.points - .map( - (point) => point.copyWith( - x: (point.x - topLeft.dx) * scaleX + position.dx, - y: (point.y - topLeft.dy) * scaleY + position.dy, - ), - ) - .toList(); + final points = element.points; + if (points.isEmpty) return const []; + var left = points.first.x; + var top = points.first.y; + for (var i = 1; i < points.length; i++) { + left = min(left, points[i].x); + top = min(top, points[i].y); + } + return [ + for (final point in points) + point.copyWith( + x: (point.x - left) * scaleX + position.dx, + y: (point.y - top) * scaleY + position.dy, + ), + ]; } Rect moveRect( diff --git a/app/lib/renderers/renderer.dart b/app/lib/renderers/renderer.dart index c4829b206f10..6c7e8892521f 100644 --- a/app/lib/renderers/renderer.dart +++ b/app/lib/renderers/renderer.dart @@ -591,14 +591,12 @@ abstract class Renderer { final topRight = _transformPoint(r.topRight, center, radians, shear); final bottomLeft = _transformPoint(r.bottomLeft, center, radians, shear); final bottomRight = _transformPoint(r.bottomRight, center, radians, shear); - final all = [topLeft, topRight, bottomLeft, bottomRight]; - final xs = all.map((p) => p.dx); - final ys = all.map((p) => p.dy); - final left = xs.reduce(min); - final right = xs.reduce(max); - final top = ys.reduce(min); - final bottom = ys.reduce(max); - return Rect.fromLTRB(left, top, right, bottom); + return Rect.fromLTRB( + min(min(topLeft.dx, topRight.dx), min(bottomLeft.dx, bottomRight.dx)), + min(min(topLeft.dy, topRight.dy), min(bottomLeft.dy, bottomRight.dy)), + max(max(topLeft.dx, topRight.dx), max(bottomLeft.dx, bottomRight.dx)), + max(max(topLeft.dy, topRight.dy), max(bottomLeft.dy, bottomRight.dy)), + ); } static Rect _inverseAabbFor( @@ -612,17 +610,15 @@ abstract class Renderer { return center + Offset(rotated.dx - rotated.dy * shear, rotated.dy); } - final points = [ - inverse(r.topLeft), - inverse(r.topRight), - inverse(r.bottomRight), - inverse(r.bottomLeft), - ]; + final topLeft = inverse(r.topLeft); + final topRight = inverse(r.topRight); + final bottomRight = inverse(r.bottomRight); + final bottomLeft = inverse(r.bottomLeft); return Rect.fromLTRB( - points.map((point) => point.dx).reduce(min), - points.map((point) => point.dy).reduce(min), - points.map((point) => point.dx).reduce(max), - points.map((point) => point.dy).reduce(max), + min(min(topLeft.dx, topRight.dx), min(bottomLeft.dx, bottomRight.dx)), + min(min(topLeft.dy, topRight.dy), min(bottomLeft.dy, bottomRight.dy)), + max(max(topLeft.dx, topRight.dx), max(bottomLeft.dx, bottomRight.dx)), + max(max(topLeft.dy, topRight.dy), max(bottomLeft.dy, bottomRight.dy)), ); } diff --git a/app/test/cubits/editor_renderer_test.dart b/app/test/cubits/editor_renderer_test.dart index 99ab378b488c..27af6c2245eb 100644 --- a/app/test/cubits/editor_renderer_test.dart +++ b/app/test/cubits/editor_renderer_test.dart @@ -1,8 +1,41 @@ import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; +import 'package:butterfly/cubits/transform.dart'; +import 'package:butterfly/models/viewport.dart'; +import 'package:butterfly/renderers/renderer.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; +class _CountingRenderer extends Renderer { + _CountingRenderer(this.bounds) : super(ShapeElement()); + + final Rect bounds; + var boundsReads = 0; + + @override + Rect get rect => bounds; + + @override + Rect get expandedRect { + boundsReads++; + return bounds; + } + + @override + void build( + Canvas canvas, + Size size, + NoteData document, + DocumentPage page, + DocumentInfo info, + CameraTransform transform, [ + ColorScheme? colorScheme, + bool foreground = false, + ]) {} +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -25,4 +58,28 @@ void main() { await subscription.cancel(); await cubit.close(); }); + + test('spatial index reuses renderer bounds between queries', () async { + SharedPreferences.setMockInitialValues({}); + final preferences = await SharedPreferences.getInstance(); + final settings = SettingsCubit(preferences); + final renderer = _CountingRenderer(const Rect.fromLTWH(10, 10, 20, 20)); + final cubit = RendererCubit( + settings, + RendererRuntimeState( + cameraViewport: CameraViewport.unbaked(unbakedElements: [renderer]), + ), + ); + + expect(cubit.visibleRenderers(const Rect.fromLTWH(0, 0, 100, 100)), [ + renderer, + ]); + expect(cubit.visibleRenderers(const Rect.fromLTWH(5, 5, 50, 50)), [ + renderer, + ]); + expect(renderer.boundsReads, 1); + + await cubit.close(); + await settings.close(); + }); } From 30954ec6027abd7eabaf3c9fc1136c0cfecd4f78 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Wed, 15 Jul 2026 13:31:43 +0200 Subject: [PATCH 091/117] Fix live pen stroke preview throttling --- app/lib/cubits/editor_tool.dart | 1 + app/lib/helpers/async.dart | 10 ++++-- app/test/helpers/async_test.dart | 57 ++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 app/test/helpers/async_test.dart diff --git a/app/lib/cubits/editor_tool.dart b/app/lib/cubits/editor_tool.dart index ead56fac9720..91e5d3ccb9c1 100644 --- a/app/lib/cubits/editor_tool.dart +++ b/app/lib/cubits/editor_tool.dart @@ -40,6 +40,7 @@ class ToolCubit extends Cubit { final foregroundRefreshRunner = CoalescedAsyncRunner(delay: Duration.zero); final delayedForegroundRefreshRunner = CoalescedAsyncRunner( delay: const Duration(milliseconds: 16), + restartDelay: false, ); EditorController? _controller; Timer? _networkingDebounceTimer; diff --git a/app/lib/helpers/async.dart b/app/lib/helpers/async.dart index 72e97249611e..cc6043819f20 100644 --- a/app/lib/helpers/async.dart +++ b/app/lib/helpers/async.dart @@ -1,9 +1,13 @@ import 'dart:async'; class CoalescedAsyncRunner { - CoalescedAsyncRunner({required this.delay}); + CoalescedAsyncRunner({required this.delay, this.restartDelay = true}); final Duration delay; + + /// Whether a new task postpones a pending run by [delay]. + /// Set to false to coalesce calls at a fixed maximum frame rate. + final bool restartDelay; Timer? _timer; Future? _running; Future Function()? _pendingTask; @@ -14,8 +18,8 @@ class CoalescedAsyncRunner { if (_disposed) return Future.value(); _pendingTask = task; final completer = _pendingCompleter ??= Completer(); - _timer?.cancel(); - if (_running == null) { + if (_running == null && (restartDelay || _timer == null)) { + _timer?.cancel(); _timer = Timer(delay, _runPending); } return completer.future; diff --git a/app/test/helpers/async_test.dart b/app/test/helpers/async_test.dart new file mode 100644 index 000000000000..c6af04fb9cd7 --- /dev/null +++ b/app/test/helpers/async_test.dart @@ -0,0 +1,57 @@ +import 'dart:async'; + +import 'package:butterfly/helpers/async.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('throttled runner is not postponed by continuous scheduling', ( + tester, + ) async { + final runner = CoalescedAsyncRunner( + delay: const Duration(milliseconds: 16), + restartDelay: false, + ); + var runs = 0; + + unawaited( + runner.schedule(() async { + runs++; + }), + ); + await tester.pump(const Duration(milliseconds: 8)); + unawaited( + runner.schedule(() async { + runs++; + }), + ); + await tester.pump(const Duration(milliseconds: 8)); + + expect(runs, 1); + await runner.disposeAndWait(); + }); + + testWidgets('default runner retains debounce behavior', (tester) async { + final runner = CoalescedAsyncRunner( + delay: const Duration(milliseconds: 16), + ); + var runs = 0; + + unawaited( + runner.schedule(() async { + runs++; + }), + ); + await tester.pump(const Duration(milliseconds: 8)); + unawaited( + runner.schedule(() async { + runs++; + }), + ); + await tester.pump(const Duration(milliseconds: 8)); + + expect(runs, 0); + await tester.pump(const Duration(milliseconds: 8)); + expect(runs, 1); + await runner.disposeAndWait(); + }); +} From e2cf957d241eff266f1cb574b5874ade857a4953 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Wed, 15 Jul 2026 17:12:07 +0200 Subject: [PATCH 092/117] Fix german description --- metadata/de-DE/full_description.txt | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/metadata/de-DE/full_description.txt b/metadata/de-DE/full_description.txt index 0a1ac33c9e2f..9c47ed8c5059 100644 --- a/metadata/de-DE/full_description.txt +++ b/metadata/de-DE/full_description.txt @@ -1,16 +1,16 @@ -Butterfly ist eine Notiz-App, bei der Ihre Ideen an erster Stellen Sie stehen. Sie können zeichnen, Texte hinzufügen und Ihre Notizen auf jedem Gerät ganz einfach exportieren. Diese App funktioniert auf Android, Windows, Linux und im Web. +Butterfly ist eine Notiz-App, bei der Ihre Ideen an erster Stelle stehen. Sie können zeichnen, Texte hinzufügen und Ihre Notizen ganz einfach exportieren. Die App ist für Android, Windows, Linux und das Web verfügbar. -* Einfach und intuitiv: Jedes Werkzeug ist genau dort, wo Sie es erwartest. Öffnen Sie die App und starte. Ändern Sie Ihre Werkzeuge, indem Sie auf sie klicken. -* Anpassbar: Passen Sie alles an Ihre Bedürfnisse an. Wählen Sie eine individuelle Farbe, erstellen Sie Ihre eigene Farbpalette und fügen Sie diese den Seiten hinzu. Das Papier hat eine unendliche Größe, perfekt für Ihre Ideen und Noten. -* Unterstützt Ihre bevorzugten Formate: Import und Export werden für Bilder, PDF und SVG unterstützt. Öffnen Sie diese Dateien direkt, um sie zu bearbeiten. -* Funktioniert auf jedem Gerät: Die App ist für Android, Windows, Linux und im Web verfügbar. Sie können es auf Ihrem Smartphone, Tablet oder Computer verwenden. -* Wählen Sie, wo Ihre Daten gespeichert werden: Sie können Ihre Daten lokal oder in Ihrer bevorzugten Cloud (WebDAV) speichern. Alternativ können Sie Ihre Daten in eine Datei exportieren und später wieder importieren. -* **In vielen Sprachen verfügbar:** Die App ist mehrsprachig. Helfen Sie uns, diese App in Ihre Sprache zu übersetzen. -* **FOSS (Open Source & kostenlos):** Der Quellcode ist offen, und die Nutzung ist gratis. Sie können aktiv zum Projekt beitragen und es weiter verbessern. -* **Offline nutzbar:** Sie können die App offline benutzen. Auch ohne Internetverbindung können Sie zeichnen, malen und Ihre Notizen exportieren. -* **Unterstützt Stylus & Touch:** Egal ob mit dem Finger oder Ihrem Lieblings-Stylus – Butterfly funktioniert auf allen Touch-Geräten. Sie können mit Ihrem bevorzugten Stil zeichnen und malen. -* **Texte schreiben:** Füge Texte ein und passe Schriftart, Größe und Farbe nach Belieben an. Sie können Schriftart, Größe und Farbe ändern. -* **Fotos aufnehmen:** Erstellen Sie direkt Fotos und fügen Sie sie Ihren Notizen hinzu oder importieren Sie Bilder aus Ihrer Galerie. Sie können auch Fotos aus Ihrer Galerie importieren. -* **Voll bearbeitbar:** Nachträgliche Änderungen an Größe, Farbe und Position jedes Elements sind jederzeit möglich. -* **Formen hinzufügen:** Wählen Sie zwischen Rechtecken, Kreisen und Linien, um Ihre Notizen visuell zu strukturieren. Sie können zwischen Rechteck, Kreis und Linie wählen. -* **Notizen strukturieren:** Lege Bereiche und Wegpunkte an, um Ihre Inhalte übersichtlich zu organisieren. +* **Einfach und intuitiv:** Jedes Werkzeug befindet sich dort, wo Sie es erwarten. Öffnen Sie die App und legen Sie direkt los. Werkzeuge lassen sich einfach auswählen und anpassen. +* **Individuell anpassbar:** Passen Sie die App an Ihre Bedürfnisse an. Wählen Sie eigene Farben, erstellen Sie individuelle Farbpaletten und verwenden Sie diese in Ihren Notizen. Die Zeichenfläche ist unbegrenzt und bietet ausreichend Platz für all Ihre Ideen. +* **Unterstützt gängige Dateiformate:** Importieren und exportieren Sie Bilder, PDF- und SVG-Dateien. Unterstützte Dateien können direkt geöffnet und weiterbearbeitet werden. +* **Auf vielen Geräten verfügbar:** Nutzen Sie Butterfly auf Android, Windows, Linux oder direkt im Web – auf Ihrem Smartphone, Tablet oder Computer. +* **Freie Wahl des Speicherorts:** Speichern Sie Ihre Daten lokal oder in Ihrer bevorzugten Cloud über WebDAV. Alternativ können Sie Ihre Notizen als Datei exportieren und später wieder importieren. +* **In vielen Sprachen verfügbar:** Butterfly ist mehrsprachig. Helfen Sie uns dabei, die App in weitere Sprachen zu übersetzen. +* **FOSS – quelloffen und kostenlos:** Der Quellcode ist öffentlich zugänglich und die Nutzung der App ist kostenlos. Sie können aktiv zum Projekt beitragen und Butterfly gemeinsam mit uns verbessern. +* **Offline nutzbar:** Verwenden Sie die App auch ohne Internetverbindung. Sie können jederzeit zeichnen, malen, schreiben und Ihre Notizen exportieren. +* **Unterstützt Stylus und Touch:** Zeichnen Sie mit dem Finger oder Ihrem bevorzugten Eingabestift. Butterfly funktioniert auf einer Vielzahl von Touch-Geräten. +* **Texte hinzufügen:** Fügen Sie Texte ein und passen Sie Schriftart, Größe und Farbe nach Ihren Wünschen an. +* **Fotos und Bilder einfügen:** Nehmen Sie direkt ein Foto auf und fügen Sie es Ihrer Notiz hinzu oder importieren Sie Bilder aus Ihrer Galerie. +* **Vollständig bearbeitbar:** Ändern Sie Größe, Farbe und Position Ihrer Elemente jederzeit nachträglich. +* **Formen hinzufügen:** Verwenden Sie Rechtecke, Kreise und Linien, um Ihre Notizen visuell zu gestalten und zu strukturieren. +* **Notizen organisieren:** Erstellen Sie Bereiche und Wegpunkte, um auch umfangreiche Inhalte übersichtlich zu organisieren. From 4bda36304b8bafc0613b46f02e1fc1062dd07533 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Wed, 15 Jul 2026 17:18:21 +0200 Subject: [PATCH 093/117] Fix rotated canvas viewport performance --- app/lib/cubits/editor_renderer.dart | 24 ++++++++++---- app/test/cubits/editor_renderer_test.dart | 38 +++++++++++++++++++++++ 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/app/lib/cubits/editor_renderer.dart b/app/lib/cubits/editor_renderer.dart index 3b42d0a8756f..b9416b6b030e 100644 --- a/app/lib/cubits/editor_renderer.dart +++ b/app/lib/cubits/editor_renderer.dart @@ -250,20 +250,32 @@ class RendererCubit extends Cubit { CameraTransform transform, RenderResolution resolution, ) { - final screenRect = transform.globalToLocalRect(rect); + final position = transform.position; + final scale = transform.size; + final scaledRect = Rect.fromLTRB( + (rect.left - position.dx) * scale, + (rect.top - position.dy) * scale, + (rect.right - position.dx) * scale, + (rect.bottom - position.dy) * scale, + ); final snappedRect = _expandScreenRect( Rect.fromLTRB( - screenRect.left.floorToDouble(), - screenRect.top.floorToDouble(), - screenRect.right.ceilToDouble(), - screenRect.bottom.ceilToDouble(), + scaledRect.left.floorToDouble(), + scaledRect.top.floorToDouble(), + scaledRect.right.ceilToDouble(), + scaledRect.bottom.ceilToDouble(), ), Size( (size.width * resolution.multiplier).ceilToDouble(), (size.height * resolution.multiplier).ceilToDouble(), ), ); - return transform.localToGlobalRect(snappedRect); + return Rect.fromLTRB( + snappedRect.left / scale + position.dx, + snappedRect.top / scale + position.dy, + snappedRect.right / scale + position.dx, + snappedRect.bottom / scale + position.dy, + ); } Rect _expandScreenRect(Rect rect, Size minimumSize) { diff --git a/app/test/cubits/editor_renderer_test.dart b/app/test/cubits/editor_renderer_test.dart index 27af6c2245eb..15c80786328e 100644 --- a/app/test/cubits/editor_renderer_test.dart +++ b/app/test/cubits/editor_renderer_test.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/cubits/transform.dart'; @@ -82,4 +84,40 @@ void main() { await cubit.close(); await settings.close(); }); + + test('rotated viewport snapping does not amplify its bounds', () async { + SharedPreferences.setMockInitialValues({}); + final preferences = await SharedPreferences.getInstance(); + final settings = SettingsCubit(preferences); + final cubit = RendererCubit(settings); + final transformCubit = TransformCubit(1)..teleport(Offset.zero, 1, pi / 4); + const viewportSize = Size(1000, 600); + + final rawBounds = transformCubit.state.localToGlobalRect( + Offset.zero & viewportSize, + ); + final snappedBounds = cubit.getViewportRect( + transformCubit, + viewportSize: viewportSize, + ); + final resolution = settings.state.renderResolution.multiplier; + + expect(cubit.rectContains(snappedBounds, rawBounds), isTrue); + expect( + snappedBounds.width, + lessThanOrEqualTo( + max(rawBounds.width, viewportSize.width * resolution) + 2, + ), + ); + expect( + snappedBounds.height, + lessThanOrEqualTo( + max(rawBounds.height, viewportSize.height * resolution) + 2, + ), + ); + + await transformCubit.close(); + await cubit.close(); + await settings.close(); + }); } From 7e44d1c9508b6902deeac7a618483d267a3d9c39 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Wed, 15 Jul 2026 23:06:25 +0200 Subject: [PATCH 094/117] Improve negative scaling --- app/lib/renderers/elements/shape.dart | 5 +- app/lib/renderers/foregrounds/select.dart | 32 ++++++++-- app/lib/renderers/renderer.dart | 43 +++++++++----- .../renderers/select_foreground_test.dart | 37 ++++++++++++ app/test/renderers/shape_renderer_test.dart | 58 +++++++++++++++++++ .../en-US/changelogs/{188.txt => 189.txt} | 1 + 6 files changed, 154 insertions(+), 22 deletions(-) rename metadata/en-US/changelogs/{188.txt => 189.txt} (98%) diff --git a/app/lib/renderers/elements/shape.dart b/app/lib/renderers/elements/shape.dart index d1c03d8f56b8..ed5b40e383a1 100644 --- a/app/lib/renderers/elements/shape.dart +++ b/app/lib/renderers/elements/shape.dart @@ -354,13 +354,16 @@ class ShapeRenderer extends Renderer { final previous = rect.topLeft; final localFirst = element.firstPosition.toOffset() - previous; final localSecond = element.secondPosition.toOffset() - previous; + final nextRotation = element.property.shape is TriangleShape && scaleY < 0 + ? (rotation + 180) % 360 + : rotation; return ShapeRenderer( element.copyWith( shear: shear, firstPosition: (localFirst.scale(scaleX, scaleY) + position).toPoint(), secondPosition: (localSecond.scale(scaleX, scaleY) + position) .toPoint(), - rotation: rotation, + rotation: nextRotation, ), layer, ); diff --git a/app/lib/renderers/foregrounds/select.dart b/app/lib/renderers/foregrounds/select.dart index 818e3a8cb400..f396a0bb5257 100644 --- a/app/lib/renderers/foregrounds/select.dart +++ b/app/lib/renderers/foregrounds/select.dart @@ -233,9 +233,26 @@ class RectSelectionForegroundManager { moved = delta; } if (_scaleMode == SelectionScaleMode.scaleProp) { - final scale = max(scaleX, scaleY); + final scale = (scaleX - 1).abs() > (scaleY - 1).abs() ? scaleX : scaleY; scaleX = scale; scaleY = scale; + moved = switch (_corner) { + SelectionTransformCorner.topLeft => Offset( + _selection.width * (1 - scale), + _selection.height * (1 - scale), + ), + SelectionTransformCorner.topCenter || + SelectionTransformCorner.topRight => Offset( + 0, + _selection.height * (1 - scale), + ), + SelectionTransformCorner.centerLeft || + SelectionTransformCorner.bottomLeft => Offset( + _selection.width * (1 - scale), + 0, + ), + _ => Offset.zero, + }; } return ( scaleX: scaleX, @@ -253,11 +270,14 @@ class RectSelectionForegroundManager { Rect getTransformedSelection() { final transform = getTransform(); if (transform == null) return _selection; - return Rect.fromLTWH( - _selection.left + transform.position.dx, - _selection.top + transform.position.dy, - _selection.width * transform.scaleX, - _selection.height * transform.scaleY, + final topLeft = _selection.topLeft + transform.position; + return Rect.fromPoints( + topLeft, + topLeft + + Offset( + _selection.width * transform.scaleX, + _selection.height * transform.scaleY, + ), ); } diff --git a/app/lib/renderers/renderer.dart b/app/lib/renderers/renderer.dart index 6c7e8892521f..d404796d709e 100644 --- a/app/lib/renderers/renderer.dart +++ b/app/lib/renderers/renderer.dart @@ -694,8 +694,8 @@ abstract class Renderer { } // Compose the requested world transform with the current local transform. - // QR decomposition then gives a rotation, an X shear and local scales, - // which together can represent every non-reflecting affine transform. + // QR decomposition then gives a rotation, an X shear and signed local + // scales. Keeping those scales signed lets _transform preserve reflections. final currentShear = shear; final c = cos(radians), s = sin(radians); final a = c; @@ -704,13 +704,27 @@ abstract class Renderer { final e = s * currentShear + c; final deltaRadians = rotationDelta * pi / 180; final dc = cos(deltaRadians), ds = sin(deltaRadians); - final m00 = dc * scaleX * a - ds * scaleY * d; - final m01 = dc * scaleX * b - ds * scaleY * e; - final m10 = ds * scaleX * a + dc * scaleY * d; - final m11 = ds * scaleX * b + dc * scaleY * e; - - final r00 = sqrt(m00 * m00 + m10 * m10); - if (r00 <= 1e-12) return null; + // Keep the transform decomposable at the exact instant a resize handle + // crosses an axis. Visually this is indistinguishable from zero size, but + // avoids briefly restoring the original element because of a singular + // matrix. + const minimumScale = 1e-9; + final effectiveScaleX = scaleX.abs() < minimumScale + ? (scaleX.isNegative ? -minimumScale : minimumScale) + : scaleX; + final effectiveScaleY = scaleY.abs() < minimumScale + ? (scaleY.isNegative ? -minimumScale : minimumScale) + : scaleY; + final m00 = dc * effectiveScaleX * a - ds * effectiveScaleY * d; + final m01 = dc * effectiveScaleX * b - ds * effectiveScaleY * e; + final m10 = ds * effectiveScaleX * a + dc * effectiveScaleY * d; + final m11 = ds * effectiveScaleX * b + dc * effectiveScaleY * e; + + final r00Magnitude = sqrt(m00 * m00 + m10 * m10); + if (r00Magnitude <= 1e-12) return null; + // Choose the QR sign nearest the element's current X axis. This preserves + // horizontal mirrors as a negative X scale instead of a 180° rotation. + final r00 = m00 * c + m10 * s < 0 ? -r00Magnitude : r00Magnitude; final q00 = m00 / r00, q10 = m10 / r00; final r01 = q00 * m01 + q10 * m11; final determinant = m00 * m11 - m01 * m10; @@ -719,15 +733,14 @@ abstract class Renderer { final nextRotation = atan2(q10, q00) * 180 / pi % 360; final geometryScaleX = r00; - final geometryScaleY = r11.abs(); + final geometryScaleY = r11; final nextShear = r01 / r11; final oldExpanded = _expandedAabbFor(rect, radians, currentShear); - final scaledRect = Rect.fromLTWH( - rect.left, - rect.top, - rect.width * geometryScaleX, - rect.height * geometryScaleY, + final scaledRect = Rect.fromPoints( + rect.topLeft, + rect.topLeft + + Offset(rect.width * geometryScaleX, rect.height * geometryScaleY), ); final newExpanded = _expandedAabbFor( scaledRect, diff --git a/app/test/renderers/select_foreground_test.dart b/app/test/renderers/select_foreground_test.dart index 42b41f6e57d0..225338457dd9 100644 --- a/app/test/renderers/select_foreground_test.dart +++ b/app/test/renderers/select_foreground_test.dart @@ -54,6 +54,43 @@ void main() { SelectionTransformCorner.center, ); }); + + test('dragging a handle across an axis keeps a valid mirrored selection', () { + final manager = RectSelectionForegroundManager() + ..select(const Rect.fromLTWH(0, 0, 100, 50)) + ..startTransformWithCorner( + SelectionTransformCorner.bottomRight, + const Offset(100, 50), + ) + ..updateCurrentPosition(const Offset(-50, 100)); + + final transform = manager.getTransform()!; + expect(transform.scaleX, -0.5); + expect(transform.scaleY, 2); + expectRectCloseTo( + manager.getTransformedSelection(), + const Rect.fromLTWH(-50, 0, 50, 100), + ); + }); + + test('proportional negative scale keeps the opposite corner anchored', () { + final manager = RectSelectionForegroundManager() + ..select(const Rect.fromLTWH(0, 0, 100, 50)) + ..transform( + SelectionScaleMode.scaleProp, + SelectionTransformCorner.topLeft, + ) + ..startTransformWithCorner(SelectionTransformCorner.topLeft, Offset.zero) + ..updateCurrentPosition(const Offset(150, 0)); + + final transform = manager.getTransform()!; + expect(transform.scaleX, -0.5); + expect(transform.scaleY, -0.5); + expectRectCloseTo( + manager.getTransformedSelection(), + const Rect.fromLTWH(100, 50, 50, 25), + ); + }); } void expectRectCloseTo(Rect actual, Rect expected) { diff --git a/app/test/renderers/shape_renderer_test.dart b/app/test/renderers/shape_renderer_test.dart index ec82423b9fca..872be7ccff7b 100644 --- a/app/test/renderers/shape_renderer_test.dart +++ b/app/test/renderers/shape_renderer_test.dart @@ -96,6 +96,64 @@ void main() { expect(PadElement.fromJson(json).shear, 0); }); + test('negative horizontal scale mirrors the element', () { + final renderer = ShapeRenderer( + ShapeElement( + firstPosition: const Point(0, 0), + secondPosition: const Point(100, 50), + ), + ); + + final mirrored = renderer.transform(scaleX: -1)!; + final mirroredElement = mirrored.element; + expect( + mirroredElement.firstPosition.x, + greaterThan(mirroredElement.secondPosition.x), + ); + + final restored = mirrored.transform(scaleX: -1)!; + final restoredElement = restored.element; + expect( + restoredElement.firstPosition.x, + lessThan(restoredElement.secondPosition.x), + ); + }); + + test('negative scale positions from the original transformed origin', () { + final renderer = ShapeRenderer( + ShapeElement( + firstPosition: const Point(10, 0), + secondPosition: const Point(30, 50), + ), + ); + + // Mirroring around x = 0 maps the original left edge (10) to -5 and + // the original right edge (30) to -15 at a scale of -0.5. + final mirrored = renderer.transform( + position: const Offset(-15, 0), + scaleX: -0.5, + )!; + + expect(mirrored.element.firstPosition.x, closeTo(-5, 1e-9)); + expect(mirrored.element.secondPosition.x, closeTo(-15, 1e-9)); + }); + + test('negative vertical scale mirrors a triangle', () { + final renderer = ShapeRenderer( + ShapeElement( + firstPosition: const Point(0, 0), + secondPosition: const Point(100, 50), + property: const ShapeProperty(shape: TriangleShape()), + ), + ); + + final mirrored = renderer.transform(scaleY: -1)!; + expect(mirrored.rotation, closeTo(180, 1e-9)); + + final restored = mirrored.transform(scaleY: -1)!; + expect(restored.rotation, closeTo(0, 1e-9)); + }); + test('rotated pen stroke is hit outside its original bounds', () { final hitCalculator = PenRenderer( PenElement( diff --git a/metadata/en-US/changelogs/188.txt b/metadata/en-US/changelogs/189.txt similarity index 98% rename from metadata/en-US/changelogs/188.txt rename to metadata/en-US/changelogs/189.txt index 3520c099c718..baf73cb26d0b 100644 --- a/metadata/en-US/changelogs/188.txt +++ b/metadata/en-US/changelogs/189.txt @@ -10,6 +10,7 @@ * Add settings descriptions * Refactor whole state management structure ([#1157](https://github.com/LinwoodDev/Butterfly/pull/1157)) * Remove unused view options +* Improve negative scaling * Improve rotated elements transformtion ([#1099](https://github.com/LinwoodDev/Butterfly/issues/1099)) * Fix crash with android saf on folders with many files * Fix blur resetting on color change From 1c8a8563b4d45cc3303143c7f0b8f6d94e890476 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Wed, 15 Jul 2026 23:25:53 +0200 Subject: [PATCH 095/117] Improve rendering reliability if baking fails --- app/lib/actions/settings.dart | 24 +++++++++----- app/lib/cubits/editor_renderer.dart | 51 ++++++++++++++++++++++++++--- metadata/en-US/changelogs/189.txt | 1 + 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/app/lib/actions/settings.dart b/app/lib/actions/settings.dart index 51afad225858..06521f441753 100644 --- a/app/lib/actions/settings.dart +++ b/app/lib/actions/settings.dart @@ -31,15 +31,23 @@ class SettingsAction extends Action { Future openSettings(BuildContext context) => showGeneralDialog( context: context, pageBuilder: (context, animation, secondaryAnimation) => ScaffoldMessenger( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), - child: Dialog( - clipBehavior: Clip.antiAlias, - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 800, maxWidth: 1000), - child: const SettingsPage(inView: true), + child: Stack( + alignment: Alignment.center, + children: [ + Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: const SizedBox.expand(), + ), ), - ), + Dialog( + clipBehavior: Clip.antiAlias, + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 800, maxWidth: 1000), + child: const SettingsPage(inView: true), + ), + ), + ], ), ), barrierDismissible: true, diff --git a/app/lib/cubits/editor_renderer.dart b/app/lib/cubits/editor_renderer.dart index b9416b6b030e..4a45514ffd71 100644 --- a/app/lib/cubits/editor_renderer.dart +++ b/app/lib/cubits/editor_renderer.dart @@ -103,6 +103,7 @@ class RendererCubit extends Cubit { _RendererSpatialIndex? _spatialIndex; List>? _indexedRenderers; Set>? _indexedUnbaked; + bool _useDirectRendering = false; void bindController(EditorController controller) { _controller = controller; @@ -455,8 +456,6 @@ class RendererCubit extends Cubit { size /= resolution.multiplier; } var transform = transformCubit.state; - final recorder = ui.PictureRecorder(); - final canvas = ui.Canvas(recorder); final rect = rendererCubit.getViewportRect( transformCubit, viewportSize: size, @@ -529,8 +528,6 @@ class RendererCubit extends Cubit { targetSize: size, ); - canvas.scale(ratio); - if (viewChanged && visibleElements.isNotEmpty) { await Future.wait( visibleElements.map( @@ -543,6 +540,43 @@ class RendererCubit extends Cubit { // Wait one frame await Future.delayed(const Duration(milliseconds: 1)); + void useDirectRendering() { + if (controller.isClosed || + !identical(rendererCubit.state.cameraViewport, startViewport) || + transformCubit.state != startTransform) { + return; + } + rendererCubit._invalidateSpatialIndex(); + rendererCubit.setViewport( + cameraViewport + .unbake( + unbakedElements: renderers, + visibleElements: visibleElements, + visibleUnbakedElements: visibleElements, + rendererStates: allRendererStates, + ) + .copyWith( + width: size.width, + height: size.height, + viewportSize: measuredViewportSize, + pixelRatio: ratio, + resolution: resolution, + scale: transform.size, + x: renderTransform.position.dx, + y: renderTransform.position.dy, + invisibleLayers: invisibleLayers, + ), + ); + } + + if (_useDirectRendering) { + useDirectRendering(); + return; + } + + final recorder = ui.PictureRecorder(); + final canvas = ui.Canvas(recorder)..scale(ratio); + ViewPainter( document, page, @@ -567,6 +601,15 @@ class RendererCubit extends Cubit { ui.Image newImage; try { newImage = await picture.toImage(imageWidth, imageHeight); + } catch (error, stackTrace) { + _useDirectRendering = true; + talker.warning( + 'Viewport image baking failed; using direct rendering', + error, + stackTrace, + ); + useDirectRendering(); + return; } finally { picture.dispose(); } diff --git a/metadata/en-US/changelogs/189.txt b/metadata/en-US/changelogs/189.txt index baf73cb26d0b..099e8a509bf6 100644 --- a/metadata/en-US/changelogs/189.txt +++ b/metadata/en-US/changelogs/189.txt @@ -12,6 +12,7 @@ * Remove unused view options * Improve negative scaling * Improve rotated elements transformtion ([#1099](https://github.com/LinwoodDev/Butterfly/issues/1099)) +* Improve rendering reliability if baking fails * Fix crash with android saf on folders with many files * Fix blur resetting on color change * Fix polygon collision aabb tests if closed ([#1162](https://github.com/LinwoodDev/Butterfly/pull/1162)) From d904cf2c7fcd89fad63421148143b56078e44ecb Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Wed, 15 Jul 2026 23:25:53 +0200 Subject: [PATCH 096/117] Improve rendering reliability if baking fails --- app/lib/actions/settings.dart | 24 ++++++++++----- app/lib/cubits/current_index.dart | 49 ++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/app/lib/actions/settings.dart b/app/lib/actions/settings.dart index fe2c247c583c..9a01ce4a60d0 100644 --- a/app/lib/actions/settings.dart +++ b/app/lib/actions/settings.dart @@ -31,15 +31,23 @@ class SettingsAction extends Action { Future openSettings(BuildContext context) => showGeneralDialog( context: context, pageBuilder: (context, animation, secondaryAnimation) => ScaffoldMessenger( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), - child: Dialog( - clipBehavior: Clip.antiAlias, - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 800, maxWidth: 1000), - child: const SettingsPage(isDialog: true), + child: Stack( + alignment: Alignment.center, + children: [ + Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: const SizedBox.expand(), + ), ), - ), + Dialog( + clipBehavior: Clip.antiAlias, + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 800, maxWidth: 1000), + child: const SettingsPage(isDialog: true), + ), + ), + ], ), ), barrierDismissible: true, diff --git a/app/lib/cubits/current_index.dart b/app/lib/cubits/current_index.dart index c2a5638df367..cf8d4e500c55 100644 --- a/app/lib/cubits/current_index.dart +++ b/app/lib/cubits/current_index.dart @@ -1253,6 +1253,7 @@ class CurrentIndexCubit extends Cubit { final _delayedBakeRunner = CoalescedAsyncRunner( delay: const Duration(milliseconds: 100), ); + bool _useDirectRendering = false; bool _rectContains(Rect outer, Rect inner) { const tolerance = precisionErrorTolerance; @@ -1284,8 +1285,6 @@ class CurrentIndexCubit extends Cubit { } var transform = state.transformCubit.state; var renderers = List>.from(this.renderers); - final recorder = ui.PictureRecorder(); - final canvas = ui.Canvas(recorder); final rect = getViewportRect(viewportSize: size); size = rect.size * transform.size; final renderTransform = transform.improve(resolution, rect); @@ -1357,8 +1356,6 @@ class CurrentIndexCubit extends Cubit { targetSize: size, ); - canvas.scale(ratio); - if (viewChanged && visibleElements.isNotEmpty) { await Future.wait( visibleElements.map( @@ -1371,6 +1368,41 @@ class CurrentIndexCubit extends Cubit { // Wait one frame await Future.delayed(const Duration(milliseconds: 1)); + void useDirectRendering() { + if (isClosed || + !identical(state.cameraViewport, startViewport) || + state.transformCubit.state != startTransform) { + return; + } + emit( + state.copyWith( + cameraViewport: CameraViewport.unbaked( + backgrounds: cameraViewport.backgrounds, + unbakedElements: renderers, + visibleElements: visibleElements, + visibleUnbakedElements: visibleElements, + width: size.width, + height: size.height, + pixelRatio: ratio, + resolution: resolution, + scale: transform.size, + x: renderTransform.position.dx, + y: renderTransform.position.dy, + rendererStates: allRendererStates, + invisibleLayers: invisibleLayers, + ), + ), + ); + } + + if (_useDirectRendering) { + useDirectRendering(); + return; + } + + final recorder = ui.PictureRecorder(); + final canvas = ui.Canvas(recorder)..scale(ratio); + ViewPainter( document, page, @@ -1395,6 +1427,15 @@ class CurrentIndexCubit extends Cubit { ui.Image newImage; try { newImage = await picture.toImage(imageWidth, imageHeight); + } catch (error, stackTrace) { + _useDirectRendering = true; + talker.warning( + 'Viewport image baking failed; using direct rendering', + error, + stackTrace, + ); + useDirectRendering(); + return; } finally { picture.dispose(); } From 41ad9e2a74750798e32a043b7d0c905b5f34871d Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Thu, 16 Jul 2026 16:25:55 +0200 Subject: [PATCH 097/117] Fix placement when flipping rotated elements --- app/lib/handlers/select.dart | 18 ++--- app/lib/renderers/elements/pen.dart | 6 +- app/lib/renderers/elements/polygon.dart | 6 +- app/lib/renderers/elements/shape.dart | 62 +++++++------- app/lib/renderers/foregrounds/select.dart | 24 +++--- app/lib/renderers/renderer.dart | 4 +- app/pubspec.lock | 20 ++--- app/test/renderers/polygon_renderer_test.dart | 18 +++++ .../renderers/select_foreground_test.dart | 16 ++++ app/test/renderers/shape_renderer_test.dart | 81 ++++++++++++++++++- 10 files changed, 184 insertions(+), 71 deletions(-) diff --git a/app/lib/handlers/select.dart b/app/lib/handlers/select.dart index 58cfe1b9598b..405b9343bb96 100644 --- a/app/lib/handlers/select.dart +++ b/app/lib/handlers/select.dart @@ -114,14 +114,8 @@ class SelectHandler extends Handler { final rotation = transform.rotation; final rotationRad = rotation * pi / 180; - final Offset selectionTopLeft = selectionRect.topLeft; - final Offset movedSelectionTopLeft = selectionTopLeft + transform.position; - - Offset applyScaleAndTranslate(Offset original) { - final relative = original - selectionTopLeft; - return Offset(relative.dx * scaleX, relative.dy * scaleY) + - movedSelectionTopLeft; - } + Offset applyScaleAndTranslate(Offset original) => + transform.scalePoint(original, selectionRect); final Offset transformedPivot = applyScaleAndTranslate(pivot); @@ -134,13 +128,14 @@ class SelectHandler extends Handler { final elementRect = renderer.rect ?? Rect.zero; final originalExpandedTopLeft = elementExpandedRect.topLeft; - final translatedExpandedTopLeft = applyScaleAndTranslate( - originalExpandedTopLeft, + final transformedExpandedRect = transform.scaleRect( + elementExpandedRect, + selectionRect, ); // Delta relative to expandedRect.topLeft so it's zero at identity. // The position compensation inside transform() converts from the // expandedRect reference frame to the rect reference frame. - var delta = translatedExpandedTopLeft - originalExpandedTopLeft; + var delta = transformedExpandedRect.topLeft - originalExpandedTopLeft; if (rotation != 0) { final originalTopLeft = elementRect.topLeft; @@ -163,6 +158,7 @@ class SelectHandler extends Handler { rotation: rotation, rotatePosition: false, relative: true, + positionIsBounds: true, ) ?? renderer; }).toList(); diff --git a/app/lib/renderers/elements/pen.dart b/app/lib/renderers/elements/pen.dart index e8a815e68e65..a72eb6f8b0c5 100644 --- a/app/lib/renderers/elements/pen.dart +++ b/app/lib/renderers/elements/pen.dart @@ -322,8 +322,10 @@ class PenRenderer extends Renderer { bool expanded = false, ]) { final rect = expanded ? expandedRect : this.rect; - final size = Size(rect.width * scaleX, rect.height * scaleY); - return position & size; + return Rect.fromPoints( + position, + position + Offset(rect.width * scaleX, rect.height * scaleY), + ); } @override diff --git a/app/lib/renderers/elements/polygon.dart b/app/lib/renderers/elements/polygon.dart index f54b926d39a2..ea460fdc4088 100644 --- a/app/lib/renderers/elements/polygon.dart +++ b/app/lib/renderers/elements/polygon.dart @@ -224,8 +224,10 @@ class PolygonRenderer extends Renderer { } Rect moveRect(Offset position, double scaleX, double scaleY) { - final size = Size(rect.width * scaleX, rect.height * scaleY); - return position & size; + return Rect.fromPoints( + position, + position + Offset(rect.width * scaleX, rect.height * scaleY), + ); } @override diff --git a/app/lib/renderers/elements/shape.dart b/app/lib/renderers/elements/shape.dart index ed5b40e383a1..a9a6e4aa703d 100644 --- a/app/lib/renderers/elements/shape.dart +++ b/app/lib/renderers/elements/shape.dart @@ -1,5 +1,15 @@ part of '../renderer.dart'; +({Offset tip, Offset left, Offset right}) _trianglePoints( + Rect rect, + ShapeElement element, +) { + final flippedVertically = element.secondPosition.y < element.firstPosition.y; + return flippedVertically + ? (tip: rect.bottomCenter, left: rect.topLeft, right: rect.topRight) + : (tip: rect.topCenter, left: rect.bottomLeft, right: rect.bottomRight); +} + class ShapeRenderer extends Renderer { final _strokePaint = ElementPaintRenderer(); final _fillPaint = ElementPaintRenderer(); @@ -161,11 +171,11 @@ class ShapeRenderer extends Renderer { ..lineTo(element.secondPosition.x, element.secondPosition.y); _drawStyledPath(canvas, path, paint); } else if (shape is TriangleShape) { - final topCenter = drawRect.topCenter; + final points = _trianglePoints(drawRect, element); final path = Path() - ..moveTo(topCenter.dx, topCenter.dy) - ..lineTo(drawRect.right, drawRect.bottom) - ..lineTo(drawRect.left, drawRect.bottom) + ..moveTo(points.tip.dx, points.tip.dy) + ..lineTo(points.right.dx, points.right.dy) + ..lineTo(points.left.dx, points.left.dy) ..close(); canvas.drawPath( path, @@ -305,11 +315,11 @@ class ShapeRenderer extends Renderer { }, ); } else if (shape is TriangleShape) { - final topCenter = drawRect.topCenter; + final points = _trianglePoints(drawRect, element); final d = - 'M${topCenter.dx} ${topCenter.dy} ' - 'L${drawRect.right} ${drawRect.bottom} ' - 'L${drawRect.left} ${drawRect.bottom} Z'; + 'M${points.tip.dx} ${points.tip.dy} ' + 'L${points.right.dx} ${points.right.dy} ' + 'L${points.left.dx} ${points.left.dy} Z'; xml .getElement('svg') ?.createElement( @@ -322,23 +332,6 @@ class ShapeRenderer extends Renderer { 'stroke-dasharray': ?dashArray, }, ); - } else if (shape is TriangleShape) { - final topCenter = drawRect.topCenter; - final d = - 'M${topCenter.dx} ${topCenter.dy} ' - 'L${drawRect.right} ${drawRect.bottom} ' - 'L${drawRect.left} ${drawRect.bottom} Z'; - xml - .getElement('svg') - ?.createElement( - 'path', - attributes: { - 'd': d, - 'fill': shape.fillPaint.previewColor.toHexString(), - 'stroke': element.property.paint.previewColor.toHexString(), - 'stroke-width': '${element.property.strokeWidth}px', - }, - ); } } @@ -354,16 +347,13 @@ class ShapeRenderer extends Renderer { final previous = rect.topLeft; final localFirst = element.firstPosition.toOffset() - previous; final localSecond = element.secondPosition.toOffset() - previous; - final nextRotation = element.property.shape is TriangleShape && scaleY < 0 - ? (rotation + 180) % 360 - : rotation; return ShapeRenderer( element.copyWith( shear: shear, firstPosition: (localFirst.scale(scaleX, scaleY) + position).toPoint(), secondPosition: (localSecond.scale(scaleX, scaleY) + position) .toPoint(), - rotation: nextRotation, + rotation: rotation, ), layer, ); @@ -503,9 +493,10 @@ class ShapeHitCalculator extends HitCalculator { } bool hitTriangle() { - final triTop = this.rect.topCenter.rotate(center, rotation); - final triLeft = this.rect.bottomLeft.rotate(center, rotation); - final triRight = this.rect.bottomRight.rotate(center, rotation); + final points = _trianglePoints(this.rect, element); + final triTop = points.tip.rotate(center, rotation); + final triLeft = points.left.rotate(center, rotation); + final triRight = points.right.rotate(center, rotation); return switch (hitElementMode) { HitElementMode.full => @@ -600,9 +591,10 @@ class ShapeHitCalculator extends HitCalculator { _ => false, // this shouldn't happen }; case TriangleShape(): - final topCenter = rect.topCenter.rotate(center, rotation); - final bottomLeft = rect.bottomLeft.rotate(center, rotation); - final bottomRight = rect.bottomRight.rotate(center, rotation); + final points = _trianglePoints(rect, element); + final topCenter = points.tip.rotate(center, rotation); + final bottomLeft = points.left.rotate(center, rotation); + final bottomRight = points.right.rotate(center, rotation); var triPoints = [topCenter, bottomLeft, bottomRight]; final inside = isPolygonInPolygon(polygon, triPoints); return switch (hitElementMode) { diff --git a/app/lib/renderers/foregrounds/select.dart b/app/lib/renderers/foregrounds/select.dart index f396a0bb5257..896dc0cd79f7 100644 --- a/app/lib/renderers/foregrounds/select.dart +++ b/app/lib/renderers/foregrounds/select.dart @@ -51,6 +51,20 @@ typedef TransformResult = ({ double scaleY, }); +extension TransformResultGeometry on TransformResult { + Offset scalePoint(Offset point, Rect selection) { + final relative = point - selection.topLeft; + return selection.topLeft + + position + + Offset(relative.dx * scaleX, relative.dy * scaleY); + } + + Rect scaleRect(Rect rect, Rect selection) => Rect.fromPoints( + scalePoint(rect.topLeft, selection), + scalePoint(rect.bottomRight, selection), + ); +} + class RectSelectionForegroundManager { final bool enableRotation; Rect _selection = Rect.zero; @@ -270,15 +284,7 @@ class RectSelectionForegroundManager { Rect getTransformedSelection() { final transform = getTransform(); if (transform == null) return _selection; - final topLeft = _selection.topLeft + transform.position; - return Rect.fromPoints( - topLeft, - topLeft + - Offset( - _selection.width * transform.scaleX, - _selection.height * transform.scaleY, - ), - ); + return transform.scaleRect(_selection, _selection); } RectSelectionForegroundRenderer get renderer => diff --git a/app/lib/renderers/renderer.dart b/app/lib/renderers/renderer.dart index d404796d709e..670fcc9fbec5 100644 --- a/app/lib/renderers/renderer.dart +++ b/app/lib/renderers/renderer.dart @@ -676,6 +676,7 @@ abstract class Renderer { double? rotation, bool relative = true, bool rotatePosition = false, + bool positionIsBounds = false, }) { final rect = this.rect ?? Rect.zero; rotation ??= relative ? 0 : this.rotation; @@ -748,9 +749,10 @@ abstract class Renderer { nextShear, ); if (rotationDelta == 0) { + final scaledOrigin = positionIsBounds ? rect.topLeft : scaledRect.topLeft; nextPosition += (oldExpanded.topLeft - rect.topLeft) - - (newExpanded.topLeft - scaledRect.topLeft); + (newExpanded.topLeft - scaledOrigin); } return _transform( diff --git a/app/pubspec.lock b/app/pubspec.lock index 0742be7383b8..961fc5c29eca 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -252,10 +252,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: cad0e811a289ea2a941119dc483c204ec1684cbb9a8fc7351fe4a230b8313160 + sha256: "25cebb79dfe304022550e0c3c893ae051ded07183d2607f7b88473cb3ade33cb" url: "https://pub.dev" source: hosted - version: "7.2.0" + version: "7.3.0" connectivity_plus_platform_interface: dependency: transitive description: @@ -893,10 +893,10 @@ packages: dependency: "direct main" description: name: network_info_plus - sha256: "4a1217d16644ed59f88e415e2777a4c81f4da5bffb724566825e5551c6379567" + sha256: "096a700e2321507e2e587487a442b9baa7ba6cd4e70e3cc1ebbb8327d2632c2d" url: "https://pub.dev" source: hosted - version: "8.2.0" + version: "8.2.1" network_info_plus_platform_interface: dependency: transitive description: @@ -977,10 +977,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: f5c435dc0e0d461e5b32471a870f769b6a1cc46930637efe24fbc535314e78ad + sha256: "127e1751e37ffb2ff4658beeaca77bad0c27bf5f932bd3a501c2296926d4b481" url: "https://pub.dev" source: hosted - version: "10.2.0" + version: "10.2.1" package_info_plus_platform_interface: dependency: transitive description: @@ -1283,10 +1283,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "9eee8283462d91a7a1c8bdb67d08874abd75a2f8fae3bc0ca033035e375fb3d8" + sha256: "02180b01c1237b9706b663d9402b2cf2402b3407f48cce99cc19e3200f095b8a" url: "https://pub.dev" source: hosted - version: "13.2.0" + version: "13.2.1" share_plus_platform_interface: dependency: transitive description: @@ -1601,10 +1601,10 @@ packages: dependency: transitive description: name: uuid - sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" url: "https://pub.dev" source: hosted - version: "4.5.3" + version: "4.6.0" vector_graphics: dependency: transitive description: diff --git a/app/test/renderers/polygon_renderer_test.dart b/app/test/renderers/polygon_renderer_test.dart index d7a10a09b94b..9b8e353e56ee 100644 --- a/app/test/renderers/polygon_renderer_test.dart +++ b/app/test/renderers/polygon_renderer_test.dart @@ -6,6 +6,24 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:material_leap/helpers.dart'; void main() { + test('negative scale keeps valid polygon bounds', () { + final points = [ + PolygonPoint(0, 0), + PolygonPoint(100, 0), + PolygonPoint(100, 50), + ]; + final renderer = PolygonRenderer( + PolygonElement(points: points), + null, + calculatePolygonRect(points), + ); + + final mirrored = renderer.transform(scaleX: -1)!; + + expect(mirrored.rect, const Rect.fromLTWH(-100, 0, 100, 50)); + expect(mirrored.rect!.isEmpty, isFalse); + }); + group('No Bezier', () { late PolygonHitCalculator calculator; diff --git a/app/test/renderers/select_foreground_test.dart b/app/test/renderers/select_foreground_test.dart index 225338457dd9..61ecfd699c73 100644 --- a/app/test/renderers/select_foreground_test.dart +++ b/app/test/renderers/select_foreground_test.dart @@ -91,6 +91,22 @@ void main() { const Rect.fromLTWH(100, 50, 50, 25), ); }); + + test('negative scale uses the opposite element corner for placement', () { + const selection = Rect.fromLTWH(0, 0, 200, 100); + const element = Rect.fromLTWH(50, 20, 100, 40); + const transform = ( + position: Offset.zero, + rotation: 0.0, + scaleX: -1.0, + scaleY: 1.0, + ); + + expectRectCloseTo( + transform.scaleRect(element, selection), + const Rect.fromLTWH(-150, 20, 100, 40), + ); + }); } void expectRectCloseTo(Rect actual, Rect expected) { diff --git a/app/test/renderers/shape_renderer_test.dart b/app/test/renderers/shape_renderer_test.dart index 872be7ccff7b..7f073061a61d 100644 --- a/app/test/renderers/shape_renderer_test.dart +++ b/app/test/renderers/shape_renderer_test.dart @@ -90,6 +90,60 @@ void main() { expect(vertical.dy, closeTo(expectedVertical.dy, 1e-9)); }); + test('horizontal flip keeps signed width without adding a half turn', () { + final renderer = ShapeRenderer( + ShapeElement( + firstPosition: const Point(10, 20), + secondPosition: const Point(110, 70), + ), + ); + + final transformed = renderer.transform(scaleX: -1)!; + final element = transformed.element; + + expect(transformed.rotation, closeTo(0, 1e-9)); + expect( + element.secondPosition.x - element.firstPosition.x, + closeTo(-100, 1e-9), + ); + expect( + element.secondPosition.y - element.firstPosition.y, + closeTo(50, 1e-9), + ); + }); + + test('horizontal flip keeps a rotated element in the target bounds', () { + final renderer = ShapeRenderer( + ShapeElement( + rotation: 30, + firstPosition: const Point(100, 100), + secondPosition: const Point(200, 150), + ), + ); + final originalBounds = renderer.expandedRect; + final targetBounds = originalBounds.shift( + Offset(-originalBounds.width, 0), + ); + + final transformed = renderer.transform( + position: Offset(-originalBounds.width, 0), + scaleX: -1, + positionIsBounds: true, + )!; + final element = transformed.element; + final transformedBounds = transformed.expandedRect!; + + expect(transformed.rotation, closeTo(330, 1e-9)); + expect( + element.secondPosition.x - element.firstPosition.x, + closeTo(-100, 1e-9), + ); + expect(transformedBounds.left, closeTo(targetBounds.left, 1e-9)); + expect(transformedBounds.top, closeTo(targetBounds.top, 1e-9)); + expect(transformedBounds.width, closeTo(targetBounds.width, 1e-9)); + expect(transformedBounds.height, closeTo(targetBounds.height, 1e-9)); + }); + test('documents without shear keep the identity default', () { final json = ShapeElement().toJson()..remove('shear'); @@ -148,10 +202,35 @@ void main() { ); final mirrored = renderer.transform(scaleY: -1)!; - expect(mirrored.rotation, closeTo(180, 1e-9)); + expect(mirrored.rotation, closeTo(0, 1e-9)); + expect( + mirrored.element.secondPosition.y - mirrored.element.firstPosition.y, + closeTo(-50, 1e-9), + ); + final hitCalculator = mirrored.getHitCalculator(); + expect(hitCalculator.hit(const Rect.fromLTWH(10, -48, 10, 8)), isTrue); + expect(hitCalculator.hit(const Rect.fromLTWH(10, -10, 10, 8)), isFalse); final restored = mirrored.transform(scaleY: -1)!; expect(restored.rotation, closeTo(0, 1e-9)); + expect( + restored.element.secondPosition.y - restored.element.firstPosition.y, + closeTo(50, 1e-9), + ); + }); + + test('negative pen scale keeps valid cached bounds', () { + final renderer = PenRenderer( + PenElement(points: const [PathPoint(0, 0), PathPoint(100, 50)]), + null, + const Rect.fromLTWH(0, 0, 100, 50), + const Rect.fromLTWH(0, 0, 100, 50), + ); + + final mirrored = renderer.transform(scaleX: -1)!; + + expect(mirrored.rect, const Rect.fromLTWH(-100, 0, 100, 50)); + expect(mirrored.rect!.isEmpty, isFalse); }); test('rotated pen stroke is hit outside its original bounds', () { From 749346100d6fd96a91b2c6685bf884e42fcddaaa Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Thu, 16 Jul 2026 16:44:18 +0200 Subject: [PATCH 098/117] Fix locale reset when previewing files --- app/lib/embed/embedding.dart | 2 +- app/test/views/project_page_lifecycle_test.dart | 1 + metadata/en-US/changelogs/189.txt | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/lib/embed/embedding.dart b/app/lib/embed/embedding.dart index 37699748401a..3af2135f5b0f 100644 --- a/app/lib/embed/embedding.dart +++ b/app/lib/embed/embedding.dart @@ -15,7 +15,7 @@ class Embedding { Embedding({ this.save = true, this.editable = true, - this.language = '', + this.language = 'user', this.theme = 'user', bool internal = false, this.onExit, diff --git a/app/test/views/project_page_lifecycle_test.dart b/app/test/views/project_page_lifecycle_test.dart index 4057e21856de..2ce2c7524621 100644 --- a/app/test/views/project_page_lifecycle_test.dart +++ b/app/test/views/project_page_lifecycle_test.dart @@ -260,6 +260,7 @@ void main() { () => observer.lastDocumentBloc?.state is DocumentLoadSuccess, 'embedded document open', ); + verifyNever(() => settingsCubit.changeLocaleTemporarily(any())); final editorController = observer.lastDocumentBloc!.editorController; expect(editorController.transformCubit.state.position, Offset.zero); diff --git a/metadata/en-US/changelogs/189.txt b/metadata/en-US/changelogs/189.txt index 099e8a509bf6..1d211f069279 100644 --- a/metadata/en-US/changelogs/189.txt +++ b/metadata/en-US/changelogs/189.txt @@ -17,6 +17,7 @@ * Fix blur resetting on color change * Fix polygon collision aabb tests if closed ([#1162](https://github.com/LinwoodDev/Butterfly/pull/1162)) * Fix embed/web loading errors ([#1167](https://github.com/LinwoodDev/Butterfly/issues/1167)) +* Fix file previews resetting the language to the system locale * Upgrade to agb 9 Read more here: https://linwood.dev/butterfly/2.6.0-beta.2 \ No newline at end of file From 3e7dbedd44f4e13e0ec892997a3ce3d2f336bfc5 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Thu, 16 Jul 2026 16:44:18 +0200 Subject: [PATCH 099/117] Fix locale reset when previewing files --- app/lib/embed/embedding.dart | 2 +- app/test/views/project_page_lifecycle_test.dart | 5 +++++ metadata/en-US/changelogs/{189.txt => 188.txt} | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) rename metadata/en-US/changelogs/{189.txt => 188.txt} (86%) diff --git a/app/lib/embed/embedding.dart b/app/lib/embed/embedding.dart index 37699748401a..3af2135f5b0f 100644 --- a/app/lib/embed/embedding.dart +++ b/app/lib/embed/embedding.dart @@ -15,7 +15,7 @@ class Embedding { Embedding({ this.save = true, this.editable = true, - this.language = '', + this.language = 'user', this.theme = 'user', bool internal = false, this.onExit, diff --git a/app/test/views/project_page_lifecycle_test.dart b/app/test/views/project_page_lifecycle_test.dart index ed4e5ee45af6..fb7a26a05d2d 100644 --- a/app/test/views/project_page_lifecycle_test.dart +++ b/app/test/views/project_page_lifecycle_test.dart @@ -3,6 +3,7 @@ import 'package:butterfly/api/open.dart'; import 'package:butterfly/bloc/document_bloc.dart'; import 'package:butterfly/cubits/current_index.dart'; import 'package:butterfly/cubits/settings.dart'; +import 'package:butterfly/embed/embedding.dart'; import 'package:butterfly/models/defaults.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; import 'package:butterfly/views/main.dart'; @@ -21,6 +22,10 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../helpers/mocks.dart'; void main() { + test('internal embedding preserves the user language', () { + expect(Embedding(internal: true).language, 'user'); + }); + setUpAll(() { TestWidgetsFlutterBinding.ensureInitialized(); SharedPreferences.setMockInitialValues({}); diff --git a/metadata/en-US/changelogs/189.txt b/metadata/en-US/changelogs/188.txt similarity index 86% rename from metadata/en-US/changelogs/189.txt rename to metadata/en-US/changelogs/188.txt index ea7852a6ee33..569f906298b8 100644 --- a/metadata/en-US/changelogs/189.txt +++ b/metadata/en-US/changelogs/188.txt @@ -13,5 +13,6 @@ Cherry picks: * Respect hidden file extensions in Recent files * Persist the thumbnail setting * Correct the texture height label +* Fix file previews resetting the language to the system locale -Read more here: https://linwood.dev/butterfly/2.5.4 +Read more here: https://linwood.dev/butterfly/2.5.4 \ No newline at end of file From 713bdfb15f566a89d980fef2ad52ae079d81faa7 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Thu, 16 Jul 2026 17:04:05 +0200 Subject: [PATCH 100/117] Keep existing filenames when editing document names --- app/lib/views/app_bar.dart | 1 + metadata/en-US/changelogs/189.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/app/lib/views/app_bar.dart b/app/lib/views/app_bar.dart index 948a2b0f121e..caea35d41f80 100644 --- a/app/lib/views/app_bar.dart +++ b/app/lib/views/app_bar.dart @@ -353,6 +353,7 @@ class _AppBarTitleState extends State<_AppBarTitle> { _nameController.text, ); var showCurrentNameFilePath = + currentIndex.isCreating && area == null && _nameFocusNode.hasFocus && currentNameFilePath != diff --git a/metadata/en-US/changelogs/189.txt b/metadata/en-US/changelogs/189.txt index 1d211f069279..f199c094445c 100644 --- a/metadata/en-US/changelogs/189.txt +++ b/metadata/en-US/changelogs/189.txt @@ -18,6 +18,7 @@ * Fix polygon collision aabb tests if closed ([#1162](https://github.com/LinwoodDev/Butterfly/pull/1162)) * Fix embed/web loading errors ([#1167](https://github.com/LinwoodDev/Butterfly/issues/1167)) * Fix file previews resetting the language to the system locale +* Fix filename preview appearing when renaming existing documents * Upgrade to agb 9 Read more here: https://linwood.dev/butterfly/2.6.0-beta.2 \ No newline at end of file From 79cb34770c33920d857fee75931f7368cc747f2d Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Thu, 16 Jul 2026 17:13:53 +0200 Subject: [PATCH 101/117] Fix tests --- app/test/embedding_test.dart | 8 ++++++++ app/test/views/project_page_lifecycle_test.dart | 5 ----- 2 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 app/test/embedding_test.dart diff --git a/app/test/embedding_test.dart b/app/test/embedding_test.dart new file mode 100644 index 000000000000..4c86af7bcbe6 --- /dev/null +++ b/app/test/embedding_test.dart @@ -0,0 +1,8 @@ +import 'package:butterfly/embed/embedding.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('internal embedding preserves the user language', () { + expect(Embedding(internal: true).language, 'user'); + }); +} diff --git a/app/test/views/project_page_lifecycle_test.dart b/app/test/views/project_page_lifecycle_test.dart index fb7a26a05d2d..ed4e5ee45af6 100644 --- a/app/test/views/project_page_lifecycle_test.dart +++ b/app/test/views/project_page_lifecycle_test.dart @@ -3,7 +3,6 @@ import 'package:butterfly/api/open.dart'; import 'package:butterfly/bloc/document_bloc.dart'; import 'package:butterfly/cubits/current_index.dart'; import 'package:butterfly/cubits/settings.dart'; -import 'package:butterfly/embed/embedding.dart'; import 'package:butterfly/models/defaults.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; import 'package:butterfly/views/main.dart'; @@ -22,10 +21,6 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../helpers/mocks.dart'; void main() { - test('internal embedding preserves the user language', () { - expect(Embedding(internal: true).language, 'user'); - }); - setUpAll(() { TestWidgetsFlutterBinding.ensureInitialized(); SharedPreferences.setMockInitialValues({}); From 33d92ad9e82dc6019c0d99d8734ef81ed5eeab33 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Thu, 16 Jul 2026 17:08:49 +0200 Subject: [PATCH 102/117] Preserve signed shape and image dimensions --- app/lib/handlers/shape.dart | 18 +-- app/lib/renderers/elements/image.dart | 112 +++++++++--------- app/test/handlers/shape_handler_test.dart | 23 ++++ app/test/renderers/image_renderer_test.dart | 121 ++++++++++++++++++++ 4 files changed, 202 insertions(+), 72 deletions(-) diff --git a/app/lib/handlers/shape.dart b/app/lib/handlers/shape.dart index 89355d5b8ef9..c7c6027f18b3 100644 --- a/app/lib/handlers/shape.dart +++ b/app/lib/handlers/shape.dart @@ -23,22 +23,8 @@ class ShapeHandler extends PastingHandler with ColoredHandler { return [ ShapeElement( - firstPosition: - (data.property.shape is LineShape - ? rect.topLeft - : rect.topLeft.translate( - min(0, rect.width), - min(0, rect.height), - )) - .toPoint(), - secondPosition: - (data.property.shape is LineShape - ? rect.bottomRight - : rect.bottomRight.translate( - max(0, -rect.width), - max(0, -rect.height), - )) - .toPoint(), + firstPosition: rect.topLeft.toPoint(), + secondPosition: rect.bottomRight.toPoint(), property: data.property.copyWith( strokeWidth: data.property.strokeWidth / diff --git a/app/lib/renderers/elements/image.dart b/app/lib/renderers/elements/image.dart index 39d8547b000a..93bff6a6af87 100644 --- a/app/lib/renderers/elements/image.dart +++ b/app/lib/renderers/elements/image.dart @@ -11,6 +11,44 @@ class ImageRenderer extends Renderer { this.ownsImage = true, ]); + Offset get _signedSize { + final constraints = element.constraints; + var width = element.width; + var height = element.height; + if (constraints is ScaledElementConstraints) { + width *= constraints.scaleX == 0 ? 1 : constraints.scaleX; + height *= constraints.scaleY == 0 ? 1 : constraints.scaleY; + } else if (constraints is FixedElementConstraints) { + width = constraints.width == 0 ? width : constraints.width; + height = constraints.height == 0 ? height : constraints.height; + } else if (constraints is DynamicElementConstraints) { + width = constraints.width; + height = constraints.height; + final ratio = constraints.aspectRatio; + if (ratio != 0) { + if (width == 0) width = height * ratio; + if (height == 0) height = width / ratio; + } + if (constraints.includeArea) { + final areaRect = area?.rect; + if (areaRect == null) { + width = element.width; + height = element.height; + } else { + final right = element.position.x + element.width; + final areaWidth = min(areaRect.right, right) - element.position.x; + width = areaWidth <= 0 ? element.width : areaWidth; + final bottom = element.position.y + element.height; + final areaHeight = min(areaRect.bottom, bottom) - element.position.y; + height = areaHeight <= 0 ? element.height : areaHeight; + } + } + if (width == 0) width = element.width; + if (height == 0) height = element.height; + } + return Offset(width, height); + } + @override bool onAssetUpdate( NoteData document, @@ -54,12 +92,17 @@ class ImageRenderer extends Renderer { ..filterQuality = FilterQuality.medium ..isAntiAlias = true; + final signedSize = _signedSize; + canvas.save(); + canvas.translate(element.position.x, element.position.y); + canvas.scale(signedSize.dx < 0 ? -1 : 1, signedSize.dy < 0 ? -1 : 1); canvas.drawImageRect( image!, - Rect.fromLTWH(0, 0, element.width.toDouble(), element.height.toDouble()), - rect, + Rect.fromLTWH(0, 0, element.width, element.height), + Rect.fromLTWH(0, 0, signedSize.dx.abs(), signedSize.dy.abs()), paint, ); + canvas.restore(); } @override @@ -72,6 +115,14 @@ class ImageRenderer extends Renderer { if (!rect.overlaps(viewportRect)) return; // Create data url final data = element.getUriData(document, 'image/png').toString(); + final signedSize = _signedSize; + final flipX = signedSize.dx < 0; + final flipY = signedSize.dy < 0; + final svgTransform = flipX || flipY + ? 'translate(${flipX ? rect.left + rect.right : 0} ' + '${flipY ? rect.top + rect.bottom : 0}) ' + 'scale(${flipX ? -1 : 1} ${flipY ? -1 : 1})' + : null; // Create image xml .getElement('svg') @@ -83,6 +134,7 @@ class ImageRenderer extends Renderer { 'width': '${rect.width}px', 'height': '${rect.height}px', 'xlink:href': data, + 'transform': ?svgTransform, }, ); } @@ -119,60 +171,8 @@ class ImageRenderer extends Renderer { @override Rect get rect { - final constraints = element.constraints; - if (constraints is ScaledElementConstraints) { - final scaleX = constraints.scaleX <= 0 ? 1 : constraints.scaleX; - final scaleY = constraints.scaleY <= 0 ? 1 : constraints.scaleY; - return Rect.fromLTWH( - element.position.x, - element.position.y, - (element.width * scaleX).toDouble(), - (element.height * scaleY).toDouble(), - ); - } else if (constraints is FixedElementConstraints) { - var height = constraints.height; - var width = constraints.width; - if (height <= 0) height = element.height.toDouble(); - if (width <= 0) width = element.width.toDouble(); - return Rect.fromLTWH( - element.position.x, - element.position.y, - width, - height, - ); - } else if (constraints is DynamicElementConstraints) { - var width = constraints.width; - var height = constraints.height; - final ratio = constraints.aspectRatio; - if (ratio != 0) { - if (width <= 0) width = height * ratio; - if (height <= 0) height = width / ratio; - } - if (constraints.includeArea) { - final areaRect = area?.rect; - final rightArea = areaRect?.right ?? 0; - final right = element.position.x + element.width; - width = min(rightArea, right) - element.position.x; - final bottomArea = areaRect?.bottom ?? 0; - final bottom = element.position.y + element.height; - height = min(bottomArea, bottom) - element.position.y; - } - if (height <= 0) height = element.height.toDouble(); - if (width <= 0) width = element.width.toDouble(); - return Rect.fromLTWH( - element.position.x, - element.position.y, - width, - height, - ); - } else { - return Rect.fromLTWH( - element.position.x, - element.position.y, - element.width.toDouble(), - element.height.toDouble(), - ); - } + final position = element.position.toOffset(); + return Rect.fromPoints(position, position + _signedSize); } @override diff --git a/app/test/handlers/shape_handler_test.dart b/app/test/handlers/shape_handler_test.dart index 5d43e3cf1c65..f780076d2183 100644 --- a/app/test/handlers/shape_handler_test.dart +++ b/app/test/handlers/shape_handler_test.dart @@ -38,4 +38,27 @@ void main() { isNotEmpty, ); }); + + test('triangle creation preserves negative drag dimensions', () { + final handler = ShapeHandler( + ShapeTool(property: const ShapeProperty(shape: TriangleShape())), + ); + final cubit = MockEditorController(); + + final element = + handler + .transformElements( + const Rect.fromLTRB(100, 80, 10, 20), + '', + cubit, + ) + .single + as ShapeElement; + + expect(element.firstPosition.x, 100); + expect(element.firstPosition.y, 80); + expect(element.secondPosition.x, 10); + expect(element.secondPosition.y, 20); + expect(element.rotation, 0); + }); } diff --git a/app/test/renderers/image_renderer_test.dart b/app/test/renderers/image_renderer_test.dart index 4c2492198339..dbdd2b82ab13 100644 --- a/app/test/renderers/image_renderer_test.dart +++ b/app/test/renderers/image_renderer_test.dart @@ -1,9 +1,14 @@ +import 'dart:math'; +import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:archive/archive.dart'; import 'package:butterfly/bloc/document_bloc.dart'; import 'package:butterfly/cubits/editor_controller.dart'; import 'package:butterfly/cubits/transform.dart'; +import 'package:butterfly/models/viewport.dart'; import 'package:butterfly/renderers/renderer.dart'; +import 'package:butterfly/view_painter.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -24,6 +29,60 @@ Future _createImage() async { } } +Future _createStripImage() async { + final recorder = ui.PictureRecorder(); + ui.Canvas(recorder) + ..drawRect( + const ui.Rect.fromLTWH(0, 0, 5, 1), + ui.Paint()..color = const ui.Color(0xFFFF0000), + ) + ..drawRect( + const ui.Rect.fromLTWH(5, 0, 5, 1), + ui.Paint()..color = const ui.Color(0xFF0000FF), + ); + final picture = recorder.endRecording(); + try { + return await picture.toImage(10, 1); + } finally { + picture.dispose(); + } +} + +Future _render(ImageRenderer renderer) async { + final recorder = ui.PictureRecorder(); + final canvas = ui.Canvas(recorder); + ViewPainter( + NoteData(Archive()), + const DocumentPage(), + const DocumentInfo(), + cameraViewport: CameraViewport.unbaked( + unbakedElements: [renderer], + visibleElements: [renderer], + visibleUnbakedElements: [renderer], + ), + ).paint(canvas, const ui.Size(10, 1)); + final picture = recorder.endRecording(); + ui.Image? image; + try { + image = await picture.toImage(10, 1); + final data = await image.toByteData(format: ui.ImageByteFormat.rawRgba); + return data!.buffer.asUint8List(); + } finally { + image?.dispose(); + picture.dispose(); + } +} + +ui.Color _pixel(Uint8List pixels, int x) { + final offset = x * 4; + return ui.Color.fromARGB( + pixels[offset + 3], + pixels[offset], + pixels[offset + 1], + pixels[offset + 2], + ); +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -48,4 +107,66 @@ void main() { renderer.dispose(); }, ); + + test('negative image scale keeps mirrored bounds', () { + final renderer = ImageRenderer( + ImageElement(source: 'test.png', width: 100, height: 50), + ); + + final mirrored = renderer.transform(scaleX: -1)!; + + expect(mirrored.rotation, 0); + expect(mirrored.rect, const ui.Rect.fromLTWH(-100, 0, 100, 50)); + expect( + (mirrored.element.constraints as ScaledElementConstraints).scaleX, + -1, + ); + }); + + test('flipped rotated image stays in the target bounds', () { + final renderer = ImageRenderer( + ImageElement( + source: 'test.png', + width: 100, + height: 50, + position: const Point(100, 100), + rotation: 30, + ), + ); + final originalBounds = renderer.expandedRect!; + final targetBounds = originalBounds.shift( + ui.Offset(-originalBounds.width, 0), + ); + + final mirrored = renderer.transform( + position: ui.Offset(-originalBounds.width, 0), + scaleX: -1, + positionIsBounds: true, + )!; + final mirroredBounds = mirrored.expandedRect!; + + expect(mirrored.rotation, closeTo(330, 1e-9)); + expect(mirroredBounds.left, closeTo(targetBounds.left, 1e-9)); + expect(mirroredBounds.top, closeTo(targetBounds.top, 1e-9)); + expect(mirroredBounds.width, closeTo(targetBounds.width, 1e-9)); + expect(mirroredBounds.height, closeTo(targetBounds.height, 1e-9)); + }); + + test('negative image scale mirrors pixels', () async { + final image = await _createStripImage(); + final renderer = ImageRenderer( + ImageElement(source: 'test.png', width: 10, height: 1), + null, + image, + ); + final mirrored = + renderer.transform(position: const ui.Offset(10, 0), scaleX: -1)! + as ImageRenderer; + + final pixels = await _render(mirrored); + + expect(_pixel(pixels, 2), const ui.Color(0xFF0000FF)); + expect(_pixel(pixels, 7), const ui.Color(0xFFFF0000)); + renderer.dispose(); + }); } From 0260ccb038350584161a98538f3376584fc8d209 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Thu, 16 Jul 2026 20:03:41 +0200 Subject: [PATCH 103/117] Add flip horizontal and vertical to all elements, closes #1058 --- app/lib/dialogs/elements.dart | 71 ++++++++++++++------- app/lib/handlers/select.dart | 3 +- app/lib/renderers/elements/image.dart | 8 --- app/lib/renderers/renderer.dart | 27 ++++++++ app/test/renderers/shape_renderer_test.dart | 22 +++++++ metadata/en-US/changelogs/189.txt | 1 + 6 files changed, 100 insertions(+), 32 deletions(-) diff --git a/app/lib/dialogs/elements.dart b/app/lib/dialogs/elements.dart index 9791b2037c91..cd2d97cb9b35 100644 --- a/app/lib/dialogs/elements.dart +++ b/app/lib/dialogs/elements.dart @@ -26,12 +26,14 @@ ContextMenuBuilder buildElementsContextMenu( ) { final cubit = bloc.editorController; final settingsCubit = state.settingsCubit; - final operations = - Map< - Renderer, - Map - >.fromIterable(renderers, value: (e) => e.getOperations()); - final operationKeys = operations.values.expand((e) => e.keys).toList(); + final operations = { + for (final renderer in renderers) renderer: renderer.getOperations(), + }; + final operationKeys = { + ...operations.values.expand((e) => e.keys), + RendererOperation.flipHorizontal, + RendererOperation.flipVertical, + }; return (context) { final rendererContextMenuItem = renderers.length == 1 ? renderers.first.getContextMenuItem(bloc, context) @@ -142,26 +144,30 @@ ContextMenuBuilder buildElementsContextMenu( .toList(), label: AppLocalizations.of(context).arrange, ), - if (operationKeys.isNotEmpty) - ContextMenuGroup( - label: AppLocalizations.of(context).operations, - icon: const PhosphorIcon(PhosphorIconsLight.wrench), - children: operationKeys - .map( - (e) => MenuItemButton( - leadingIcon: PhosphorIcon(e.icon(PhosphorIconsStyle.light)), - child: Text(e.getLocalizedName(context)), - onPressed: () { + ContextMenuGroup( + label: AppLocalizations.of(context).operations, + icon: const PhosphorIcon(PhosphorIconsLight.wrench), + children: operationKeys + .map( + (e) => MenuItemButton( + leadingIcon: PhosphorIcon(e.icon(PhosphorIconsStyle.light)), + child: Text(e.getLocalizedName(context)), + onPressed: () { + final flipAxis = e.flipAxis; + if (flipAxis == null) { operations.values .map((v) => v[e]) .nonNulls .forEach((e) => e(bloc, context)); - if (context.mounted) Navigator.of(context).pop(true); - }, - ), - ) - .toList(), - ), + } else { + _flipElements(bloc, renderers, rect, flipAxis); + } + if (context.mounted) Navigator.of(context).pop(true); + }, + ), + ) + .toList(), + ), ?rendererContextMenuItem, if (renderers.length == 1 && exportService.isExportable(renderers.first.element)) @@ -199,3 +205,24 @@ ContextMenuBuilder buildElementsContextMenu( ]; }; } + +void _flipElements( + DocumentBloc bloc, + List> renderers, + Rect selectionRect, + Axis axis, +) { + final changes = Map.fromEntries( + renderers.map((renderer) { + final id = renderer.element.id; + if (id == null) return null; + final transformed = renderer.flip( + axis: axis, + selectionRect: selectionRect, + ); + if (transformed == null) return null; + return MapEntry(id, [transformed.element]); + }).nonNulls, + ); + if (changes.isNotEmpty) bloc.add(ElementsChanged(changes)); +} diff --git a/app/lib/handlers/select.dart b/app/lib/handlers/select.dart index 405b9343bb96..b8106bc303e1 100644 --- a/app/lib/handlers/select.dart +++ b/app/lib/handlers/select.dart @@ -334,7 +334,6 @@ class SelectHandler extends Handler { ); final hit = hits.firstOrNull; final rect = hit?.expandedRect; - final selectionRect = getSelectionRect(); final hitSelection = _isSelectionHit( position, context.getCameraTransform(), @@ -358,7 +357,7 @@ class SelectHandler extends Handler { context.getClipboardManager(), localPosition, _selected, - selectionRect, + getSelectionRect(), ), ); if (result ?? false) { diff --git a/app/lib/renderers/elements/image.dart b/app/lib/renderers/elements/image.dart index 93bff6a6af87..8023977372e8 100644 --- a/app/lib/renderers/elements/image.dart +++ b/app/lib/renderers/elements/image.dart @@ -232,14 +232,6 @@ class ImageRenderer extends Renderer { updateImage(bloc, (cmd) => cmd.filter(updateImageBackground())), RendererOperation.grayscale: (bloc, context) => updateImage(bloc, (cmd) => cmd.grayscale()), - RendererOperation.flipHorizontal: (bloc, context) => updateImage( - bloc, - (cmd) => cmd.flip(direction: img.FlipDirection.horizontal), - ), - RendererOperation.flipVertical: (bloc, context) => updateImage( - bloc, - (cmd) => cmd.flip(direction: img.FlipDirection.vertical), - ), }; } } diff --git a/app/lib/renderers/renderer.dart b/app/lib/renderers/renderer.dart index 670fcc9fbec5..616c3b15aa45 100644 --- a/app/lib/renderers/renderer.dart +++ b/app/lib/renderers/renderer.dart @@ -474,6 +474,12 @@ enum RendererOperation { RendererOperation.flipHorizontal => PhosphorIcons.flipHorizontal, RendererOperation.flipVertical => PhosphorIcons.flipVertical, }; + + Axis? get flipAxis => switch (this) { + RendererOperation.flipHorizontal => Axis.horizontal, + RendererOperation.flipVertical => Axis.vertical, + _ => null, + }; } typedef RendererOperationCallback = @@ -764,6 +770,27 @@ abstract class Renderer { ); } + Renderer? flip({required Axis axis, required Rect selectionRect}) { + final bounds = expandedRect ?? rect; + if (bounds == null) return null; + final offset = switch (axis) { + Axis.horizontal => Offset( + 2 * (selectionRect.center.dx - bounds.center.dx), + 0, + ), + Axis.vertical => Offset( + 0, + 2 * (selectionRect.center.dy - bounds.center.dy), + ), + }; + return transform( + position: offset, + scaleX: axis == Axis.horizontal ? -1 : 1, + scaleY: axis == Axis.vertical ? -1 : 1, + positionIsBounds: true, + ); + } + Renderer? _transform({ required Offset position, required double rotation, diff --git a/app/test/renderers/shape_renderer_test.dart b/app/test/renderers/shape_renderer_test.dart index 7f073061a61d..424adecb4bde 100644 --- a/app/test/renderers/shape_renderer_test.dart +++ b/app/test/renderers/shape_renderer_test.dart @@ -4,6 +4,7 @@ import 'dart:ui'; import 'package:butterfly/renderers/renderer.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/widgets.dart' show Axis; void main() { group('Zero Size Shapes', () { @@ -112,6 +113,27 @@ void main() { ); }); + test('flip mirrors the renderer inside the selection bounds', () { + final renderer = ShapeRenderer( + ShapeElement( + firstPosition: const Point(120, 210), + secondPosition: const Point(220, 260), + ), + ); + + final horizontal = renderer.flip( + axis: Axis.horizontal, + selectionRect: const Rect.fromLTWH(100, 200, 200, 100), + )!; + final vertical = renderer.flip( + axis: Axis.vertical, + selectionRect: const Rect.fromLTWH(100, 200, 200, 100), + )!; + + expect(horizontal.expandedRect, const Rect.fromLTWH(180, 210, 100, 50)); + expect(vertical.expandedRect, const Rect.fromLTWH(120, 240, 100, 50)); + }); + test('horizontal flip keeps a rotated element in the target bounds', () { final renderer = ShapeRenderer( ShapeElement( diff --git a/metadata/en-US/changelogs/189.txt b/metadata/en-US/changelogs/189.txt index f199c094445c..bf3a00233c54 100644 --- a/metadata/en-US/changelogs/189.txt +++ b/metadata/en-US/changelogs/189.txt @@ -4,6 +4,7 @@ * Add custom fonts to label ([#1011](https://github.com/LinwoodDev/Butterfly/issues/1011)) * Add option to customize default file name globally and in template ([#1041](https://github.com/LinwoodDev/Butterfly/issues/1041)) * Reorder top corner menu to have home on top ([#1161](https://github.com/LinwoodDev/Butterfly/issues/1161)) +* Add flip horizontal and vertical to all elements ([#1058](https://github.com/LinwoodDev/Butterfly/issues/1058)) * Rebuild internal settings pages * Add search bar to settings pages ([#1158](https://github.com/LinwoodDev/Butterfly/issues/1158)) * Always have settings value on the right side From c734563fee7d3043bcf9e04846c3822bf2ae3a57 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 19 Jul 2026 13:48:03 +0200 Subject: [PATCH 104/117] Fix flipped PDF elements behaving unpredictably and fix elements jumping when rotated past 90 degree, closes #1171, closes #1172 --- app/lib/renderers/elements/pdf.dart | 110 ++++++++++---------- app/lib/renderers/renderer.dart | 7 +- app/test/renderers/pdf_renderer_test.dart | 38 +++++++ app/test/renderers/shape_renderer_test.dart | 18 ++++ 4 files changed, 115 insertions(+), 58 deletions(-) create mode 100644 app/test/renderers/pdf_renderer_test.dart diff --git a/app/lib/renderers/elements/pdf.dart b/app/lib/renderers/elements/pdf.dart index 15df82a06f96..ccb6ece1b07c 100644 --- a/app/lib/renderers/elements/pdf.dart +++ b/app/lib/renderers/elements/pdf.dart @@ -14,6 +14,44 @@ class PdfRenderer extends Renderer { this.ownsImage = true, ]); + Offset get _signedSize { + final constraints = element.constraints; + var width = element.width; + var height = element.height; + if (constraints is ScaledElementConstraints) { + width *= constraints.scaleX == 0 ? 1 : constraints.scaleX; + height *= constraints.scaleY == 0 ? 1 : constraints.scaleY; + } else if (constraints is FixedElementConstraints) { + width = constraints.width == 0 ? width : constraints.width; + height = constraints.height == 0 ? height : constraints.height; + } else if (constraints is DynamicElementConstraints) { + width = constraints.width; + height = constraints.height; + final ratio = constraints.aspectRatio; + if (ratio != 0) { + if (width == 0) width = height * ratio; + if (height == 0) height = width / ratio; + } + if (constraints.includeArea) { + final areaRect = area?.rect; + if (areaRect == null) { + width = element.width; + height = element.height; + } else { + final right = element.position.x + element.width; + final areaWidth = min(areaRect.right, right) - element.position.x; + width = areaWidth == 0 ? element.width : areaWidth; + final bottom = element.position.y + element.height; + final areaHeight = min(areaRect.bottom, bottom) - element.position.y; + height = areaHeight == 0 ? element.height : areaHeight; + } + } + if (width == 0) width = element.width; + if (height == 0) height = element.height; + } + return Offset(width, height); + } + @override bool onAssetUpdate( NoteData document, @@ -53,12 +91,17 @@ class PdfRenderer extends Renderer { final paint = Paint() ..filterQuality = FilterQuality.high ..isAntiAlias = true; + final signedSize = _signedSize; + canvas.save(); + canvas.translate(element.position.x, element.position.y); + canvas.scale(signedSize.dx < 0 ? -1 : 1, signedSize.dy < 0 ? -1 : 1); canvas.drawImageRect( image, Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()), - rect, + Rect.fromLTWH(0, 0, signedSize.dx.abs(), signedSize.dy.abs()), paint, ); + canvas.restore(); } } @@ -72,6 +115,14 @@ class PdfRenderer extends Renderer { if (!rect.overlaps(viewportRect)) return; // Create data url final data = element.getUriData(document, 'image/png').toString(); + final signedSize = _signedSize; + final flipX = signedSize.dx < 0; + final flipY = signedSize.dy < 0; + final svgTransform = flipX || flipY + ? 'translate(${flipX ? rect.left + rect.right : 0} ' + '${flipY ? rect.top + rect.bottom : 0}) ' + 'scale(${flipX ? -1 : 1} ${flipY ? -1 : 1})' + : null; // Create image xml .getElement('svg') @@ -83,6 +134,7 @@ class PdfRenderer extends Renderer { 'width': '${rect.width}px', 'height': '${rect.height}px', 'xlink:href': data, + 'transform': ?svgTransform, }, ); } @@ -219,60 +271,8 @@ class PdfRenderer extends Renderer { @override Rect get rect { - final constraints = element.constraints; - if (constraints is ScaledElementConstraints) { - final scaleX = constraints.scaleX <= 0 ? 1 : constraints.scaleX; - final scaleY = constraints.scaleY <= 0 ? 1 : constraints.scaleY; - return Rect.fromLTWH( - element.position.x, - element.position.y, - (element.width * scaleX).toDouble(), - (element.height * scaleY).toDouble(), - ); - } else if (constraints is FixedElementConstraints) { - var height = constraints.height; - var width = constraints.width; - if (height <= 0) height = element.height.toDouble(); - if (width <= 0) width = element.width.toDouble(); - return Rect.fromLTWH( - element.position.x, - element.position.y, - width, - height, - ); - } else if (constraints is DynamicElementConstraints) { - var width = constraints.width; - var height = constraints.height; - final ratio = constraints.aspectRatio; - if (ratio != 0) { - if (width <= 0) width = height * ratio; - if (height <= 0) height = width / ratio; - } - if (constraints.includeArea) { - final areaRect = area?.rect; - final rightArea = areaRect?.right ?? 0; - final right = element.position.x + element.width; - width = min(rightArea, right) - element.position.x; - final bottomArea = areaRect?.bottom ?? 0; - final bottom = element.position.y + element.height; - height = min(bottomArea, bottom) - element.position.y; - } - if (height <= 0) height = element.height.toDouble(); - if (width <= 0) width = element.width.toDouble(); - return Rect.fromLTWH( - element.position.x, - element.position.y, - width, - height, - ); - } else { - return Rect.fromLTWH( - element.position.x, - element.position.y, - element.width.toDouble(), - element.height.toDouble(), - ); - } + final position = element.position.toOffset(); + return Rect.fromPoints(position, position + _signedSize); } @override diff --git a/app/lib/renderers/renderer.dart b/app/lib/renderers/renderer.dart index 616c3b15aa45..74d084820d06 100644 --- a/app/lib/renderers/renderer.dart +++ b/app/lib/renderers/renderer.dart @@ -729,9 +729,10 @@ abstract class Renderer { final r00Magnitude = sqrt(m00 * m00 + m10 * m10); if (r00Magnitude <= 1e-12) return null; - // Choose the QR sign nearest the element's current X axis. This preserves - // horizontal mirrors as a negative X scale instead of a 180° rotation. - final r00 = m00 * c + m10 * s < 0 ? -r00Magnitude : r00Magnitude; + // Resolve the QR sign ambiguity from the requested X scale. Comparing the + // transformed axis with the current axis would turn rotations beyond 90° + // into a reflection plus a half turn, making elements jump while rotating. + final r00 = effectiveScaleX.isNegative ? -r00Magnitude : r00Magnitude; final q00 = m00 / r00, q10 = m10 / r00; final r01 = q00 * m01 + q10 * m11; final determinant = m00 * m11 - m01 * m10; diff --git a/app/test/renderers/pdf_renderer_test.dart b/app/test/renderers/pdf_renderer_test.dart new file mode 100644 index 000000000000..6d5015922644 --- /dev/null +++ b/app/test/renderers/pdf_renderer_test.dart @@ -0,0 +1,38 @@ +import 'dart:ui'; + +import 'package:butterfly/renderers/renderer.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('negative PDF scale keeps mirrored bounds', () { + final renderer = PdfRenderer( + PdfElement(source: 'test.pdf', width: 100, height: 50), + ); + + final mirrored = renderer.transform(scaleX: -1)!; + + expect(mirrored.rotation, 0); + expect(mirrored.rect, const Rect.fromLTWH(-100, 0, 100, 50)); + expect( + (mirrored.element.constraints as ScaledElementConstraints).scaleX, + -1, + ); + }); + + test('mirrored PDF remains stable through a subsequent transform', () { + final renderer = PdfRenderer( + PdfElement(source: 'test.pdf', width: 100, height: 50), + ); + + final mirrored = renderer.transform(scaleX: -1)!; + final resized = mirrored.transform(scaleX: 2)!; + + expect(resized.rotation, 0); + expect(resized.rect, const Rect.fromLTWH(-300, 0, 200, 50)); + expect( + (resized.element.constraints as ScaledElementConstraints).scaleX, + -2, + ); + }); +} diff --git a/app/test/renderers/shape_renderer_test.dart b/app/test/renderers/shape_renderer_test.dart index 424adecb4bde..c1829f3ff9a4 100644 --- a/app/test/renderers/shape_renderer_test.dart +++ b/app/test/renderers/shape_renderer_test.dart @@ -64,6 +64,24 @@ void main() { }); group('rotation test', () { + test('rotation past a quarter turn does not introduce a reflection', () { + final renderer = ShapeRenderer( + ShapeElement( + firstPosition: const Point(0, 0), + secondPosition: const Point(100, 50), + ), + ); + + for (final rotation in [91.0, 135.0, 180.0, 269.0]) { + final transformed = renderer.transform(rotation: rotation)!; + final element = transformed.element; + + expect(transformed.rotation, closeTo(rotation, 1e-9)); + expect(element.secondPosition.x - element.firstPosition.x, 100); + expect(element.secondPosition.y - element.firstPosition.y, 50); + } + }); + test('non-uniform scaling preserves the affine shape of a rotation', () { final renderer = ShapeRenderer( ShapeElement( From 863e7635cdc0a097100d45c77b9182dc013d34d1 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 19 Jul 2026 20:07:26 +0200 Subject: [PATCH 105/117] Save sidebar position, closes #1180 --- docs/package.json | 6 +- docs/pnpm-lock.yaml | 127 ++++++++++++++++++------------------ docs/src/scripts/pwa.ts | 3 +- docs/src/scripts/sidebar.ts | 59 +++++++++++++++++ 4 files changed, 128 insertions(+), 67 deletions(-) create mode 100644 docs/src/scripts/sidebar.ts diff --git a/docs/package.json b/docs/package.json index ee8a05710367..edb634c27400 100644 --- a/docs/package.json +++ b/docs/package.json @@ -11,15 +11,15 @@ }, "dependencies": { "@astrojs/check": "^0.9.9", - "@astrojs/markdown-satteri": "^0.3.3", + "@astrojs/markdown-satteri": "^0.3.4", "@astrojs/react": "^6.0.1", "@astrojs/starlight": "^0.41.3", "@linwooddev/style": "github:LinwoodDev/style#efbdf8f05d4ef1cf6fec65c90194d30b68b3c64e&path:/packages/web", "@phosphor-icons/react": "^2.1.10", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", - "astro": "^7.0.7", - "katex": "^0.17.0", + "astro": "^7.1.1", + "katex": "^0.18.0", "react": "^19.2.7", "react-dom": "^19.2.7", "typescript": "^6.0.3" diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index 0b0e4760715d..d2195ba944cc 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -12,14 +12,14 @@ importers: specifier: ^0.9.9 version: 0.9.9(prettier@3.9.5)(typescript@6.0.3) '@astrojs/markdown-satteri': - specifier: ^0.3.3 - version: 0.3.3 + specifier: ^0.3.4 + version: 0.3.4 '@astrojs/react': specifier: ^6.0.1 version: 6.0.1(@types/node@26.1.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) '@astrojs/starlight': specifier: ^0.41.3 - version: 0.41.3(@astrojs/markdown-remark@7.2.1)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3) + version: 0.41.3(@astrojs/markdown-remark@7.2.1)(astro@7.1.1(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3) '@linwooddev/style': specifier: github:LinwoodDev/style#efbdf8f05d4ef1cf6fec65c90194d30b68b3c64e&path:/packages/web version: https://codeload.github.com/LinwoodDev/style/tar.gz/efbdf8f05d4ef1cf6fec65c90194d30b68b3c64e#path:/packages/web @@ -33,11 +33,11 @@ importers: specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.17) astro: - specifier: ^7.0.7 - version: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + specifier: ^7.1.1 + version: 7.1.1(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) katex: - specifier: ^0.17.0 - version: 0.17.0 + specifier: ^0.18.0 + version: 0.18.0 react: specifier: ^19.2.7 version: 19.2.7 @@ -50,7 +50,7 @@ importers: devDependencies: '@vite-pwa/astro': specifier: ^1.2.0 - version: 1.2.0(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1)) + version: 1.2.0(astro@7.1.1(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1)) sass: specifier: ^1.101.0 version: 1.101.0 @@ -59,7 +59,7 @@ importers: version: 0.35.3(@types/node@26.1.1) vite-plugin-pwa: specifier: ^1.3.0 - version: 1.3.0(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1) + version: 1.3.0(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1) workbox-window: specifier: ^7.4.1 version: 7.4.1 @@ -149,8 +149,8 @@ packages: '@astrojs/internal-helpers@0.10.1': resolution: {integrity: sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q==} - '@astrojs/language-server@2.16.11': - resolution: {integrity: sha512-sJ/EfnFp0+gurTrkvONtd9qRqmMZLT9bHelfI1SA35CaQVTrRrA74qteOcNT/al1b9Atg3IiH1Jk/qfckyC+fg==} + '@astrojs/language-server@2.16.12': + resolution: {integrity: sha512-3LpFphBCzveUgm5ZVDINB/v3YA4TgPa1EMOEFn3Zt/Ww6jojR25iN+kmzeUz7v/b9xkmq+hMACTX4hizN3VCEQ==} hasBin: true peerDependencies: prettier: ^3.0.0 @@ -164,11 +164,11 @@ packages: '@astrojs/markdown-remark@7.2.1': resolution: {integrity: sha512-jPVNIqTvk+yKviikszv/Y1U4jGUSKpp/Nw48QZV4qjWgp70j4Lkq3lhSDRbWwCfgKvEyO9GHuVbV1dM2WYXy1w==} - '@astrojs/markdown-satteri@0.3.3': - resolution: {integrity: sha512-Lje33Ittd8UQGgbIIWQvhPkj5X5c4b1sZnZWX3JQV/AWpfbuQGxVi2ONt6+ScydcwfR4egilslEWyczMclrJ1g==} + '@astrojs/markdown-satteri@0.3.4': + resolution: {integrity: sha512-6Lvt/bQZEBW+zzdhPblvfZEy5PGEYJaUsUqaCgwHeRPxZJL1gc9I+DRLKWJjjYTWDzVUTzXlMq4WwSK+X34CVw==} - '@astrojs/mdx@7.0.2': - resolution: {integrity: sha512-l+sJY5U1KkGZUdr+bIL4Y6BefeS549qoSHVSkUSs6A9INwdCND+/0+vN0NroPBXwl5Vcg5u78t7VQRsJjePxbw==} + '@astrojs/mdx@7.0.3': + resolution: {integrity: sha512-RxyIwU0uFam5ftwqKOjpIdhnFxZ/kEikeimLyQy3eGXbHT8WgRGzzesOIHVU8+m9TY8ag5WVOyvV24/GyqPdPQ==} engines: {node: '>=22.12.0'} peerDependencies: '@astrojs/markdown-satteri': ^0.3.1 @@ -1836,8 +1836,8 @@ packages: peerDependencies: astro: ^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0 - astro@7.0.7: - resolution: {integrity: sha512-swqrKDSI/B83GFroYPZYMFcxqbSe9+tkynu1WDBk3GLgBfV9++qVM4Z+2uFT6uu9A53Q0TROnxLketfMEENuqQ==} + astro@7.1.1: + resolution: {integrity: sha512-yfKhuvbz+DORHihJRL1DHcxyLcX9uTrxvz2nCSTzHH54k2DdACBfuOu2OAAAhdUC9xlu2IRXAiagqcPL3DftCg==} engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true peerDependencies: @@ -2682,8 +2682,8 @@ packages: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} - katex@0.17.0: - resolution: {integrity: sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==} + katex@0.18.0: + resolution: {integrity: sha512-rZ4Sw94Oja12Ib4+ee7inAr//yxj0G8RaAlh+ZpzfFhwTifGeJbqJ7D0FStcv6EV8DeNNU9Y8rPeg6vPUizlqg==} hasBin: true kleur@4.1.5: @@ -3039,8 +3039,8 @@ packages: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} - obug@2.1.3: - resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} ofetch@1.5.1: @@ -3132,8 +3132,8 @@ packages: resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==} engines: {node: '>=4'} - postcss@8.5.17: - resolution: {integrity: sha512-J7EF+8X+CzRPaJPOv9Ck2wNWJvGnnl3PcNPAdGg6GTLjyVpyQ0yATMSXRFRV01BviT/9Gwuc3rjEyJbDJG9a4w==} + postcss@8.5.19: + resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==} engines: {node: ^10 || ^12 || >=14} prettier@3.9.5: @@ -3763,8 +3763,8 @@ packages: '@vite-pwa/assets-generator': optional: true - vite@8.1.4: - resolution: {integrity: sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==} + vite@8.1.5: + resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4055,7 +4055,7 @@ snapshots: '@astrojs/check@0.9.9(prettier@3.9.5)(typescript@6.0.3)': dependencies: - '@astrojs/language-server': 2.16.11(prettier@3.9.5)(typescript@6.0.3) + '@astrojs/language-server': 2.16.12(prettier@3.9.5)(typescript@6.0.3) chokidar: 4.0.3 kleur: 4.1.5 typescript: 6.0.3 @@ -4131,7 +4131,7 @@ snapshots: smol-toml: 1.7.0 unified: 11.0.5 - '@astrojs/language-server@2.16.11(prettier@3.9.5)(typescript@6.0.3)': + '@astrojs/language-server@2.16.12(prettier@3.9.5)(typescript@6.0.3)': dependencies: '@astrojs/compiler': 2.13.1 '@astrojs/yaml2ts': 0.2.4 @@ -4178,20 +4178,21 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/markdown-satteri@0.3.3': + '@astrojs/markdown-satteri@0.3.4': dependencies: '@astrojs/internal-helpers': 0.10.1 '@astrojs/prism': 4.0.2 github-slugger: 2.0.0 + hast-util-from-html: 2.0.3 satteri: 0.9.5 - '@astrojs/mdx@7.0.2(@astrojs/markdown-satteri@0.3.3)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': + '@astrojs/mdx@7.0.3(@astrojs/markdown-satteri@0.3.4)(astro@7.1.1(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: '@astrojs/internal-helpers': 0.10.1 '@astrojs/markdown-remark': 7.2.1 '@mdx-js/mdx': 3.1.1 acorn: 8.17.0 - astro: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + astro: 7.1.1(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) es-module-lexer: 2.3.1 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 @@ -4203,7 +4204,7 @@ snapshots: unist-util-visit: 5.1.0 vfile: 6.0.3 optionalDependencies: - '@astrojs/markdown-satteri': 0.3.3 + '@astrojs/markdown-satteri': 0.3.4 transitivePeerDependencies: - supports-color @@ -4216,12 +4217,12 @@ snapshots: '@astrojs/internal-helpers': 0.10.1 '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@vitejs/plugin-react': 5.2.0(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + '@vitejs/plugin-react': 5.2.0(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) devalue: 5.8.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) ultrahtml: 1.7.0 - vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - '@vitejs/devtools' @@ -4243,17 +4244,17 @@ snapshots: stream-replace-string: 2.0.0 zod: 4.4.3 - '@astrojs/starlight@0.41.3(@astrojs/markdown-remark@7.2.1)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3)': + '@astrojs/starlight@0.41.3(@astrojs/markdown-remark@7.2.1)(astro@7.1.1(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(typescript@6.0.3)': dependencies: - '@astrojs/markdown-satteri': 0.3.3 - '@astrojs/mdx': 7.0.2(@astrojs/markdown-satteri@0.3.3)(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + '@astrojs/markdown-satteri': 0.3.4 + '@astrojs/mdx': 7.0.3(@astrojs/markdown-satteri@0.3.4)(astro@7.1.1(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) '@astrojs/sitemap': 3.7.3 '@pagefind/default-ui': 1.5.2 '@types/hast': 3.0.5 '@types/js-yaml': 4.0.9 '@types/mdast': 4.0.4 - astro: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - astro-expressive-code: 0.44.0(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + astro: 7.1.1(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + astro-expressive-code: 0.44.0(astro@7.1.1(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) bcp-47: 2.1.1 hast-util-from-html: 2.0.3 hast-util-select: 6.0.4 @@ -5143,8 +5144,8 @@ snapshots: hast-util-to-html: 9.0.5 hast-util-to-text: 4.0.2 hastscript: 9.0.1 - postcss: 8.5.17 - postcss-nested: 6.2.0(postcss@8.5.17) + postcss: 8.5.19 + postcss-nested: 6.2.0(postcss@8.5.19) unist-util-visit: 5.1.0 unist-util-visit-parents: 6.0.2 @@ -5735,12 +5736,12 @@ snapshots: '@ungap/structured-clone@1.3.2': {} - '@vite-pwa/astro@1.2.0(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1))': + '@vite-pwa/astro@1.2.0(astro@7.1.1(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(vite-plugin-pwa@1.3.0(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1))': dependencies: - astro: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - vite-plugin-pwa: 1.3.0(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1) + astro: 7.1.1(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite-plugin-pwa: 1.3.0(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1) - '@vitejs/plugin-react@5.2.0(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': + '@vitejs/plugin-react@5.2.0(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) @@ -5748,7 +5749,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -5863,17 +5864,17 @@ snapshots: astring@1.9.0: {} - astro-expressive-code@0.44.0(astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): + astro-expressive-code@0.44.0(astro@7.1.1(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): dependencies: - astro: 7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + astro: 7.1.1(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) rehype-expressive-code: 0.44.0 url-extras: 0.1.0 - astro@7.0.7(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): + astro@7.1.1(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): dependencies: '@astrojs/compiler-rs': 0.3.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2) '@astrojs/internal-helpers': 0.10.1 - '@astrojs/markdown-satteri': 0.3.3 + '@astrojs/markdown-satteri': 0.3.4 '@astrojs/telemetry': 3.3.3 '@capsizecss/unpack': 4.0.1 '@clack/prompts': 1.7.0 @@ -5903,7 +5904,7 @@ snapshots: magicast: 0.5.3 mrmime: 2.0.1 neotraverse: 0.6.18 - obug: 2.1.3 + obug: 2.1.4 p-limit: 7.3.0 p-queue: 9.3.1 package-manager-detector: 1.7.0 @@ -5919,8 +5920,8 @@ snapshots: ultrahtml: 1.7.0 unifont: 0.7.4 unstorage: 1.17.5 - vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) - vitefu: 1.1.3(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) + vite: 8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vitefu: 1.1.3(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)) xxhash-wasm: 1.1.0 yargs-parser: 22.0.0 zod: 4.4.3 @@ -6988,7 +6989,7 @@ snapshots: jsonpointer@5.0.1: {} - katex@0.17.0: + katex@0.18.0: dependencies: commander: 8.3.0 @@ -7588,7 +7589,7 @@ snapshots: has-symbols: 1.1.0 object-keys: 1.1.1 - obug@2.1.3: {} + obug@2.1.4: {} ofetch@1.5.1: dependencies: @@ -7681,9 +7682,9 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-nested@6.2.0(postcss@8.5.17): + postcss-nested@6.2.0(postcss@8.5.19): dependencies: - postcss: 8.5.17 + postcss: 8.5.19 postcss-selector-parser: 6.1.4 postcss-selector-parser@6.1.4: @@ -7691,7 +7692,7 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss@8.5.17: + postcss@8.5.19: dependencies: nanoid: 3.3.16 picocolors: 1.1.1 @@ -8490,22 +8491,22 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-pwa@1.3.0(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1): + vite-plugin-pwa@1.3.0(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1): dependencies: debug: 4.4.3 pretty-bytes: 6.1.1 tinyglobby: 0.2.17 - vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) workbox-build: 7.4.1(@types/babel__core@7.20.5) workbox-window: 7.4.1 transitivePeerDependencies: - supports-color - vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): + vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.5 - postcss: 8.5.17 + postcss: 8.5.19 rolldown: 1.1.5 tinyglobby: 0.2.17 optionalDependencies: @@ -8516,9 +8517,9 @@ snapshots: terser: 5.48.0 yaml: 2.9.0 - vitefu@1.1.3(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): + vitefu@1.1.3(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)): optionalDependencies: - vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) + vite: 8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) volar-service-css@0.0.71(@volar/language-service@2.4.28): dependencies: diff --git a/docs/src/scripts/pwa.ts b/docs/src/scripts/pwa.ts index 719467357ecd..d7947f25bac8 100644 --- a/docs/src/scripts/pwa.ts +++ b/docs/src/scripts/pwa.ts @@ -1,4 +1,5 @@ import { registerSW } from 'virtual:pwa-register' +import './sidebar' console.log("registering SW") registerSW({ @@ -9,4 +10,4 @@ registerSW({ onOfflineReady() { console.log('PWA application ready to work offline') }, -}) \ No newline at end of file +}) diff --git a/docs/src/scripts/sidebar.ts b/docs/src/scripts/sidebar.ts new file mode 100644 index 000000000000..634b37b1bdf5 --- /dev/null +++ b/docs/src/scripts/sidebar.ts @@ -0,0 +1,59 @@ +const sidebarStorageKey = 'sl-sidebar-state'; + +interface SidebarState { + hash: string; + open: Array; + scroll: number; +} + +document.addEventListener('astro:before-preparation', () => { + const scroller = document.getElementById('starlight__sidebar'); + const stateContainer = scroller?.querySelector( + 'sl-sidebar-state-persist', + ); + if (!scroller || !stateContainer) return; + + const open: Array = []; + for (const restorePoint of stateContainer.querySelectorAll( + 'sl-sidebar-restore', + )) { + const index = Number.parseInt(restorePoint.dataset.index ?? '', 10); + const details = restorePoint.closest('details'); + if (!Number.isNaN(index) && details) open[index] = details.open; + } + + try { + sessionStorage.setItem( + sidebarStorageKey, + JSON.stringify({ + hash: stateContainer.dataset.hash ?? '', + open, + scroll: scroller.scrollTop, + }), + ); + } catch { + // Storage can be unavailable in private browsing or restricted contexts. + } +}); + +document.addEventListener('astro:page-load', () => { + const scroller = document.getElementById('starlight__sidebar'); + const stateContainer = scroller?.querySelector( + 'sl-sidebar-state-persist', + ); + if (!scroller || !stateContainer) return; + + try { + const state = JSON.parse( + sessionStorage.getItem(sidebarStorageKey) ?? 'null', + ) as SidebarState | null; + if ( + state?.hash === stateContainer.dataset.hash && + Number.isFinite(state?.scroll) + ) { + scroller.scrollTop = state?.scroll ?? 0; + } + } catch { + // Ignore malformed or inaccessible session storage. + } +}); From e3bcf3b74c4ade69f4f03f3318b3012ce071c7e2 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 19 Jul 2026 20:25:03 +0200 Subject: [PATCH 106/117] Upgrade dependencies --- .github/workflows/build.yml | 2 +- app/android/Gemfile.lock | 2 +- app/pubspec.lock | 40 +++++++++++++++---------------- app/pubspec.yaml | 2 +- app/rust-toolchain.toml | 2 +- metadata/en-US/changelogs/188.txt | 1 + 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 736822b8985e..6ae5392bf3de 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -904,7 +904,7 @@ jobs: - name: Setup Fastlane uses: ruby/setup-ruby@v1 with: - ruby-version: "4.0.5" + ruby-version: "4.0.6" bundler-cache: true working-directory: app/android - name: 🚀 Deploy to Play Store diff --git a/app/android/Gemfile.lock b/app/android/Gemfile.lock index 0a9b6c162164..2b49d84cdc05 100644 --- a/app/android/Gemfile.lock +++ b/app/android/Gemfile.lock @@ -243,4 +243,4 @@ DEPENDENCIES screengrab BUNDLED WITH - 4.0.12 + 4.0.16 diff --git a/app/pubspec.lock b/app/pubspec.lock index f637bdb7d3c0..77899ae0a5c2 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -244,10 +244,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: cad0e811a289ea2a941119dc483c204ec1684cbb9a8fc7351fe4a230b8313160 + sha256: "25cebb79dfe304022550e0c3c893ae051ded07183d2607f7b88473cb3ade33cb" url: "https://pub.dev" source: hosted - version: "7.2.0" + version: "7.3.0" connectivity_plus_platform_interface: dependency: transitive description: @@ -802,8 +802,8 @@ packages: dependency: "direct main" description: path: "packages/lw_file_system" - ref: "18711cf5297f9679701536255dfe3d650b72fd25" - resolved-ref: "18711cf5297f9679701536255dfe3d650b72fd25" + ref: "956b2f21a37a87d5824361ca30966782a9fbb467" + resolved-ref: "956b2f21a37a87d5824361ca30966782a9fbb467" url: "https://github.com/LinwoodDev/dart_pkgs.git" source: git version: "1.0.0" @@ -902,10 +902,10 @@ packages: dependency: "direct main" description: name: network_info_plus - sha256: "4a1217d16644ed59f88e415e2777a4c81f4da5bffb724566825e5551c6379567" + sha256: "096a700e2321507e2e587487a442b9baa7ba6cd4e70e3cc1ebbb8327d2632c2d" url: "https://pub.dev" source: hosted - version: "8.2.0" + version: "8.2.1" network_info_plus_platform_interface: dependency: transitive description: @@ -977,10 +977,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: f5c435dc0e0d461e5b32471a870f769b6a1cc46930637efe24fbc535314e78ad + sha256: "127e1751e37ffb2ff4658beeaca77bad0c27bf5f932bd3a501c2296926d4b481" url: "https://pub.dev" source: hosted - version: "10.2.0" + version: "10.2.1" package_info_plus_platform_interface: dependency: transitive description: @@ -1282,10 +1282,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "9eee8283462d91a7a1c8bdb67d08874abd75a2f8fae3bc0ca033035e375fb3d8" + sha256: "02180b01c1237b9706b663d9402b2cf2402b3407f48cce99cc19e3200f095b8a" url: "https://pub.dev" source: hosted - version: "13.2.0" + version: "13.2.1" share_plus_platform_interface: dependency: transitive description: @@ -1473,34 +1473,34 @@ packages: dependency: "direct main" description: name: talker - sha256: f1a14d623f1d1bec42bb3bb77674eb766ffe8d26e5f79af652d85cb097c3e757 + sha256: "342a30d81df3fc2ffca8daafdfee0c1b882269b0825dcc62093075c08bac9500" url: "https://pub.dev" source: hosted - version: "5.1.17" + version: "5.1.19" talker_bloc_logger: dependency: "direct main" description: name: talker_bloc_logger - sha256: b799c899b89472e1b2030a97f56c2ce7dfa739f743f464774c89a2c4d7c61447 + sha256: "2233c7b9009e82c5517365881a048b1a71a176f405457144b2caab5dc96cc19f" url: "https://pub.dev" source: hosted - version: "5.1.17" + version: "5.1.19" talker_flutter: dependency: "direct main" description: name: talker_flutter - sha256: "7e4b5fb520b4dadfc8db97e73a2a76ea5d6eda471a51489f3c0bd58b96a1ed43" + sha256: a37a15e2a996b6534006d3fa95c712edb07b60299c021a5cf0836f940405be47 url: "https://pub.dev" source: hosted - version: "5.1.17" + version: "5.1.19" talker_logger: dependency: transitive description: name: talker_logger - sha256: "459205c3e571f97ecc6be6e1b1b7e6b97b853e78ea458894650be407596e3216" + sha256: "999b76cb583ee89da098e8ef3a7b863eb5261326f0d33e0877f20c9df1003790" url: "https://pub.dev" source: hosted - version: "5.1.17" + version: "5.1.19" term_glyph: dependency: transitive description: @@ -1609,10 +1609,10 @@ packages: dependency: transitive description: name: uuid - sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" url: "https://pub.dev" source: hosted - version: "4.5.3" + version: "4.6.0" vector_graphics: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 3c7243656123..e558752f15ee 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -92,7 +92,7 @@ dependencies: lw_file_system: git: url: https://github.com/LinwoodDev/dart_pkgs.git - ref: 18711cf5297f9679701536255dfe3d650b72fd25 + ref: 956b2f21a37a87d5824361ca30966782a9fbb467 path: packages/lw_file_system keybinder: git: diff --git a/app/rust-toolchain.toml b/app/rust-toolchain.toml index f25b5b1407b3..72436043232c 100644 --- a/app/rust-toolchain.toml +++ b/app/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.95.0" +channel = "1.97.1" diff --git a/metadata/en-US/changelogs/188.txt b/metadata/en-US/changelogs/188.txt index 569f906298b8..762ec8310f54 100644 --- a/metadata/en-US/changelogs/188.txt +++ b/metadata/en-US/changelogs/188.txt @@ -14,5 +14,6 @@ Cherry picks: * Persist the thumbnail setting * Correct the texture height label * Fix file previews resetting the language to the system locale +* Avoid repeated WebDAV waits while offline Read more here: https://linwood.dev/butterfly/2.5.4 \ No newline at end of file From 8876a1c28490dc6e06a0bf3799d0bcbd39224c8b Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Sun, 19 Jul 2026 20:52:51 +0200 Subject: [PATCH 107/117] Fix metadata file --- metadata/en-US/changelogs/{188.txt => 189.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename metadata/en-US/changelogs/{188.txt => 189.txt} (100%) diff --git a/metadata/en-US/changelogs/188.txt b/metadata/en-US/changelogs/189.txt similarity index 100% rename from metadata/en-US/changelogs/188.txt rename to metadata/en-US/changelogs/189.txt From 09c9ee2bc9cb5760e4dbf20aa6f1e4e166e8769a Mon Sep 17 00:00:00 2001 From: Linwood CI Date: Mon, 20 Jul 2026 12:34:19 +0000 Subject: [PATCH 108/117] Add changelog of v2.5.4 --- CHANGELOG.md | 22 +++++++++++++++++++ .../dev.linwood.butterfly.appdata.xml | 1 + 2 files changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f94d91f5f57..a7f544984222 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ +## 2.5.4 (2026-07-20) + +This is a hotfix update, cherry-picking important fixes from the latest 2.6.0 beta and nightly releases. + +Cherry picks: +* Prevent crashes when Android SAF handles large folders and files +* Fix text labels disappearing while editing +* Fix polygons disappearing while editing +* Fix renamed files appearing twice in Recent files +* Fix layers and pages being reordered to the wrong position +* Improve WebDAV compatibility and select the correct filesystem with SAF enabled +* Fix the spacer tool for circles, shapes, polygons, and other elements +* Fix repeated saves and mark imported documents as unsaved +* Fix locked zoom controls +* Respect hidden file extensions in Recent files +* Persist the thumbnail setting +* Correct the texture height label +* Fix file previews resetting the language to the system locale +* Avoid repeated WebDAV waits while offline + +Read more here: https://linwood.dev/butterfly/2.5.4 + ## 2.5.3 (2026-06-08) Changes since 2.5.3-rc.1: diff --git a/app/linux/debian/usr/share/metainfo/dev.linwood.butterfly.appdata.xml b/app/linux/debian/usr/share/metainfo/dev.linwood.butterfly.appdata.xml index a8910cb03717..3ff27d8034b9 100644 --- a/app/linux/debian/usr/share/metainfo/dev.linwood.butterfly.appdata.xml +++ b/app/linux/debian/usr/share/metainfo/dev.linwood.butterfly.appdata.xml @@ -72,6 +72,7 @@ dev.linwood.butterfly.desktop + From b4700e50ef0d4b11a5ba82b89bbe37c54a226e26 Mon Sep 17 00:00:00 2001 From: Linwood CI Date: Mon, 20 Jul 2026 12:48:53 +0000 Subject: [PATCH 109/117] Update Version to 2.5.5 --- api/pubspec.yaml | 2 +- app/linux/debian/DEBIAN/control | 2 +- app/pubspec.lock | 2 +- app/pubspec.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/pubspec.yaml b/api/pubspec.yaml index 105772107978..bfd5215ffc47 100644 --- a/api/pubspec.yaml +++ b/api/pubspec.yaml @@ -1,6 +1,6 @@ name: butterfly_api description: The Linwood Butterfly API -version: 2.5.4 +version: 2.5.5 publish_to: none environment: diff --git a/app/linux/debian/DEBIAN/control b/app/linux/debian/DEBIAN/control index ea772ee14e99..b6bbecf4f754 100644 --- a/app/linux/debian/DEBIAN/control +++ b/app/linux/debian/DEBIAN/control @@ -1,5 +1,5 @@ Package: linwood-butterfly -Version: 2.5.4 +Version: 2.5.5 Section: base Priority: optional Homepage: https://github.com/LinwoodDev/butterfly diff --git a/app/pubspec.lock b/app/pubspec.lock index 77899ae0a5c2..447e2ee21f6e 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -143,7 +143,7 @@ packages: path: "../api" relative: true source: path - version: "2.5.4" + version: "2.5.5" camera: dependency: "direct main" description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index e558752f15ee..0a8ef1244684 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -13,7 +13,7 @@ publish_to: none # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 2.5.4+189 +version: 2.5.5+190 environment: sdk: ">=3.12.2 <4.0.0" From 2e6f8b18db66e382bb6d9407cc719b06ef1f894f Mon Sep 17 00:00:00 2001 From: Linwood CI Date: Mon, 20 Jul 2026 12:55:51 +0000 Subject: [PATCH 110/117] Bump version --- app/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pubspec.yaml b/app/pubspec.yaml index f6f967eee13a..d3e33c736b25 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -13,7 +13,7 @@ publish_to: none # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 2.6.0-beta.3+189 +version: 2.6.0-beta.3+190 environment: sdk: ">=3.12.2 <4.0.0" From 1b0b070fcc9825eb3e8a5197ab7132137b5f8c73 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 20 Jul 2026 17:06:56 +0200 Subject: [PATCH 111/117] Upgrade dependencies --- SECURITY.md | 2 +- api/pubspec.lock | 4 ++-- app/android/Gemfile.lock | 10 +++++----- app/android/settings.gradle.kts | 2 +- app/rust-toolchain.toml | 2 +- docs/package.json | 4 ++-- docs/pnpm-lock.yaml | 26 +++++++++++++------------- docs/pnpm-workspace.yaml | 4 ---- metadata/en-US/changelogs/190.txt | 2 +- 9 files changed, 26 insertions(+), 30 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 04ed32580b65..224728a28ea9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,7 +5,7 @@ | Version | Supported | | | -------------------------- | ------------------ | ----------------------------------------------------------------------------- | | 2.6-dev (Dreamy Duskywing) | :warning: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.6.0-beta.2) | -| 2.5.3 (Crimson Red) | :white_check_mark: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.5.3) | +| 2.5.4 (Crimson Red) | :white_check_mark: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.5.4) | | 2.4.4 (Black Hairstreak) | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.4.4) | | 2.3.4 (Adonis Blue) | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.3.4) | | 2.2.4 | :x: | [Release](https://github.com/LinwoodDev/butterfly/releases/tag/v2.2.4) | diff --git a/api/pubspec.lock b/api/pubspec.lock index ed4d48100608..eb628f083204 100644 --- a/api/pubspec.lock +++ b/api/pubspec.lock @@ -567,10 +567,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" url: "https://pub.dev" source: hosted - version: "4.5.3" + version: "4.6.0" vm_service: dependency: transitive description: diff --git a/app/android/Gemfile.lock b/app/android/Gemfile.lock index 30dd4fc09095..a46983a5c059 100644 --- a/app/android/Gemfile.lock +++ b/app/android/Gemfile.lock @@ -8,7 +8,7 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1268.0) + aws-partitions (1.1271.0) aws-sdk-core (3.254.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -20,7 +20,7 @@ GEM aws-sdk-kms (1.130.0) aws-sdk-core (~> 3, >= 3.254.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.227.0) + aws-sdk-s3 (1.228.0) aws-sdk-core (~> 3, >= 3.254.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -42,7 +42,7 @@ GEM domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (1.5.0) + excon (1.6.0) logger faraday (1.10.6) faraday-em_http (~> 1.0) @@ -126,7 +126,7 @@ GEM xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) fastlane-sirp (1.1.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.104.0) + google-apis-androidpublisher_v3 (0.105.0) google-apis-core (>= 0.15.0, < 2.a) google-apis-core (0.18.0) addressable (~> 2.5, >= 2.5.1) @@ -140,7 +140,7 @@ GEM google-apis-core (>= 0.15.0, < 2.a) google-apis-playcustomapp_v1 (0.18.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.64.0) + google-apis-storage_v1 (0.65.0) google-apis-core (>= 0.15.0, < 2.a) google-cloud-core (1.9.0) google-cloud-env (>= 1.0, < 3.a) diff --git a/app/android/settings.gradle.kts b/app/android/settings.gradle.kts index 2c69157c7661..a95d8c760792 100644 --- a/app/android/settings.gradle.kts +++ b/app/android/settings.gradle.kts @@ -19,7 +19,7 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "9.2.1" apply false - id("org.jetbrains.kotlin.android") version "2.3.21" apply false + id("org.jetbrains.kotlin.android") version "2.4.10" apply false } include(":app") diff --git a/app/rust-toolchain.toml b/app/rust-toolchain.toml index 2d45363a5be0..72436043232c 100644 --- a/app/rust-toolchain.toml +++ b/app/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.96.1" +channel = "1.97.1" diff --git a/docs/package.json b/docs/package.json index edb634c27400..59191a99afea 100644 --- a/docs/package.json +++ b/docs/package.json @@ -19,12 +19,12 @@ "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "astro": "^7.1.1", - "katex": "^0.18.0", + "katex": "^0.18.1", "react": "^19.2.7", "react-dom": "^19.2.7", "typescript": "^6.0.3" }, - "packageManager": "pnpm@11.11.0", + "packageManager": "pnpm@11.15.1", "devDependencies": { "@vite-pwa/astro": "^1.2.0", "sass": "^1.101.0", diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index d2195ba944cc..fb1a3bebe415 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -36,8 +36,8 @@ importers: specifier: ^7.1.1 version: 7.1.1(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.2)(@types/node@26.1.1)(rollup@4.62.2)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0) katex: - specifier: ^0.18.0 - version: 0.18.0 + specifier: ^0.18.1 + version: 0.18.1 react: specifier: ^19.2.7 version: 19.2.7 @@ -2682,8 +2682,8 @@ packages: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} - katex@0.18.0: - resolution: {integrity: sha512-rZ4Sw94Oja12Ib4+ee7inAr//yxj0G8RaAlh+ZpzfFhwTifGeJbqJ7D0FStcv6EV8DeNNU9Y8rPeg6vPUizlqg==} + katex@0.18.1: + resolution: {integrity: sha512-Td8GCYSxDAoMhHOlKmCFMJ/hz5qlAAb71n66Dryw9nfCVfumLo7nhuotbvKom/XPADmrYC3O5QR71EPq4DarJQ==} hasBin: true kleur@4.1.5: @@ -3132,8 +3132,8 @@ packages: resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==} engines: {node: '>=4'} - postcss@8.5.19: - resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==} + postcss@8.5.20: + resolution: {integrity: sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==} engines: {node: ^10 || ^12 || >=14} prettier@3.9.5: @@ -5144,8 +5144,8 @@ snapshots: hast-util-to-html: 9.0.5 hast-util-to-text: 4.0.2 hastscript: 9.0.1 - postcss: 8.5.19 - postcss-nested: 6.2.0(postcss@8.5.19) + postcss: 8.5.20 + postcss-nested: 6.2.0(postcss@8.5.20) unist-util-visit: 5.1.0 unist-util-visit-parents: 6.0.2 @@ -6989,7 +6989,7 @@ snapshots: jsonpointer@5.0.1: {} - katex@0.18.0: + katex@0.18.1: dependencies: commander: 8.3.0 @@ -7682,9 +7682,9 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-nested@6.2.0(postcss@8.5.19): + postcss-nested@6.2.0(postcss@8.5.20): dependencies: - postcss: 8.5.19 + postcss: 8.5.20 postcss-selector-parser: 6.1.4 postcss-selector-parser@6.1.4: @@ -7692,7 +7692,7 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss@8.5.19: + postcss@8.5.20: dependencies: nanoid: 3.3.16 picocolors: 1.1.1 @@ -8506,7 +8506,7 @@ snapshots: dependencies: lightningcss: 1.32.0 picomatch: 4.0.5 - postcss: 8.5.19 + postcss: 8.5.20 rolldown: 1.1.5 tinyglobby: 0.2.17 optionalDependencies: diff --git a/docs/pnpm-workspace.yaml b/docs/pnpm-workspace.yaml index 40576d87373a..f0ce1ab865f3 100644 --- a/docs/pnpm-workspace.yaml +++ b/docs/pnpm-workspace.yaml @@ -2,7 +2,3 @@ allowBuilds: '@parcel/watcher': true esbuild: true sharp: true -minimumReleaseAgeExclude: - - '@astrojs/markdown-satteri@0.3.2' - - '@astrojs/starlight@0.41.0' - - astro@7.0.2 diff --git a/metadata/en-US/changelogs/190.txt b/metadata/en-US/changelogs/190.txt index 8ae3a9342ffb..9047c8761be6 100644 --- a/metadata/en-US/changelogs/190.txt +++ b/metadata/en-US/changelogs/190.txt @@ -10,4 +10,4 @@ * Fix file previews resetting the language to the system locale * Fix filename preview appearing when renaming existing documents -Read more here: https://linwood.dev/butterfly/2.6.0-beta.2 \ No newline at end of file +Read more here: https://linwood.dev/butterfly/2.6.0-beta.3 \ No newline at end of file From b98c7f738ab5dc2424d8d505767dc378b2c57806 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Mon, 20 Jul 2026 17:17:55 +0200 Subject: [PATCH 112/117] Fix navigation menus broken on mobile layout, closes #1177 --- app/lib/views/app_bar.dart | 19 ++++++++ .../views/project_page_lifecycle_test.dart | 44 +++++++++++++++++++ metadata/en-US/changelogs/190.txt | 1 + 3 files changed, 64 insertions(+) diff --git a/app/lib/views/app_bar.dart b/app/lib/views/app_bar.dart index caea35d41f80..a2546c2462a0 100644 --- a/app/lib/views/app_bar.dart +++ b/app/lib/views/app_bar.dart @@ -724,6 +724,25 @@ class MainPopupMenu extends StatelessWidget { providers: [ BlocProvider.value(value: bloc), BlocProvider.value(value: transformCubit), + if (cubit.editorSessionCubit != null) + BlocProvider.value( + value: cubit.editorSessionCubit!, + ), + BlocProvider.value( + value: cubit.rendererCubit, + ), + BlocProvider.value( + value: cubit.toolCubit, + ), + BlocProvider.value( + value: cubit.inputCubit, + ), + BlocProvider.value( + value: cubit.saveCubit, + ), + BlocProvider.value( + value: cubit.viewCubit, + ), ], child: RepositoryProvider.value( value: cubit, diff --git a/app/test/views/project_page_lifecycle_test.dart b/app/test/views/project_page_lifecycle_test.dart index 2ce2c7524621..ab809b5a7025 100644 --- a/app/test/views/project_page_lifecycle_test.dart +++ b/app/test/views/project_page_lifecycle_test.dart @@ -9,6 +9,7 @@ import 'package:butterfly/models/persisted_document_state.dart'; import 'package:butterfly/services/font.dart'; import 'package:butterfly/src/generated/i18n/app_localizations.dart'; import 'package:butterfly/views/main.dart'; +import 'package:butterfly/views/navigator/view.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -208,6 +209,49 @@ void main() { expect(observer.documentBlocCloses, 3); }); + testWidgets('mobile navigator dialogs receive the editor runtime cubits', ( + tester, + ) async { + tester.view.physicalSize = const Size(400, 800); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + await tester.pumpWidget(buildApp()); + await tester.tap(find.byKey(const ValueKey('open-document'))); + await pumpUntil( + tester, + () => observer.lastDocumentBloc?.state is DocumentLoadSuccess, + 'document open', + ); + await tester.pumpAndSettle(); + + for (final page in [ + 'Waypoints', + 'Areas', + 'Layers', + 'Pages', + 'Components', + 'Files', + ]) { + await tester.tap(find.byTooltip('Actions')); + await tester.pumpAndSettle(); + await tester.tap(find.text(page)); + await tester.pumpAndSettle(); + + expect(find.byType(DocumentNavigator), findsOneWidget, reason: page); + expect(tester.takeException(), isNull, reason: page); + + await tester.tap( + find.descendant( + of: find.byType(DocumentNavigator), + matching: find.byTooltip('Close'), + ), + ); + await tester.pumpAndSettle(); + } + }); + testWidgets('converted imported file starts unsaved', (tester) async { await tester.pumpWidget(buildApp()); diff --git a/metadata/en-US/changelogs/190.txt b/metadata/en-US/changelogs/190.txt index 9047c8761be6..614c5b121aca 100644 --- a/metadata/en-US/changelogs/190.txt +++ b/metadata/en-US/changelogs/190.txt @@ -9,5 +9,6 @@ * Fix embed/web loading errors ([#1167](https://github.com/LinwoodDev/Butterfly/issues/1167)) * Fix file previews resetting the language to the system locale * Fix filename preview appearing when renaming existing documents +* Fix navigation menus broken on mobile layout ([#1177](https://github.com/LinwoodDev/Butterfly/issues/1177)) Read more here: https://linwood.dev/butterfly/2.6.0-beta.3 \ No newline at end of file From 2af3d8558a3166855f5debe2efc806fa4417cc40 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 21 Jul 2026 11:34:09 +0200 Subject: [PATCH 113/117] Upgrade dependencies --- .github/workflows/build.yml | 28 +++++++++---------- .../workflows/calibreapp-image-actions.yml | 2 +- .github/workflows/compress-images.yml | 2 +- .github/workflows/dart.yml | 2 +- .github/workflows/deploy.yml | 6 ++-- .github/workflows/release.yml | 12 ++++---- .github/workflows/update-screenshots.yml | 2 +- app/pubspec.lock | 2 +- app/pubspec.yaml | 2 +- 9 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 56155c7f89f1..adfe16365d82 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: ⬆️ Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: 🔧 Setup java uses: actions/setup-java@v5 with: @@ -153,7 +153,7 @@ jobs: working-directory: app steps: - name: ⬆️ Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Make yq tool available on Windows runners run: | choco install yq @@ -213,7 +213,7 @@ jobs: working-directory: app steps: - name: ⬆️ Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: 🦀 Setup Rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: @@ -262,7 +262,7 @@ jobs: working-directory: app steps: - name: ⬆️ Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Get dependencies run: | sudo apt-get update @@ -406,7 +406,7 @@ jobs: runs-on: ${{ matrix.arch.image }} steps: - name: ⬆️ Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Add snapcraft directory run: | mkdir -p snap @@ -433,7 +433,7 @@ jobs: working-directory: app steps: - name: ⬆️ Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: 🦀 Setup Rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: @@ -441,7 +441,7 @@ jobs: - uses: subosito/flutter-action@v2.23.0 with: flutter-version-file: app/pubspec.yaml - - uses: actions/setup-python@v6 + - uses: actions/setup-python@v7 with: python-version: "3.13" - name: ✅ Enable platforms @@ -458,7 +458,7 @@ jobs: working-directory: app/build/macos/Build/Products/Release run: zip --symlinks -qr linwood-butterfly-macos.zip butterfly.app - name: Setup node - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: 26 - name: Install appdmg @@ -489,7 +489,7 @@ jobs: working-directory: app steps: - name: ⬆️ Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: 🦀 Setup Rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: @@ -526,7 +526,7 @@ jobs: if: github.event_name != 'pull_request' steps: - name: ⬆️ Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set Docker tag id: docker_tag run: | @@ -572,7 +572,7 @@ jobs: - build-ipa steps: - name: ⬆️ Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 env: CI_PAT: ${{ secrets.CI_PAT }} with: @@ -870,7 +870,7 @@ jobs: PLAY_STORE_CREDENTIALS: ${{ secrets.PLAY_STORE_CREDENTIALS }} steps: - name: ⬆️ Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: token: ${{ secrets.CI_PAT }} - name: Setup git @@ -923,7 +923,7 @@ jobs: needs: [deploy] runs-on: windows-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: token: ${{ secrets.CI_PAT }} - if: ${{ github.ref == 'refs/tags/stable' }} @@ -1016,7 +1016,7 @@ jobs: echo "arm64=$ARM64_SHA" >> "$GITHUB_OUTPUT" - name: Checkout Flathub beta branch - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: repository: flathub/dev.linwood.butterfly ref: beta diff --git a/.github/workflows/calibreapp-image-actions.yml b/.github/workflows/calibreapp-image-actions.yml index dc5305dd3cff..2285ad0e4d7b 100644 --- a/.github/workflows/calibreapp-image-actions.yml +++ b/.github/workflows/calibreapp-image-actions.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Compress Images uses: calibreapp/image-actions@main diff --git a/.github/workflows/compress-images.yml b/.github/workflows/compress-images.yml index 795cc5d77574..64afefa9f04e 100644 --- a/.github/workflows/compress-images.yml +++ b/.github/workflows/compress-images.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Compress Images id: calibre uses: calibreapp/image-actions@main diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index b96a619b3d14..2cd09777dc2c 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -13,7 +13,7 @@ jobs: working-directory: ${{ matrix.projects }} steps: - name: ⬆️ Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - uses: subosito/flutter-action@v2.23.0 with: flutter-version-file: app/pubspec.yaml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d4d29abacde0..e654fbb02600 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,13 +14,13 @@ jobs: run: working-directory: docs steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install pnpm uses: pnpm/action-setup@v6 with: package_json_file: docs/package.json - name: Use Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: 26 cache: "pnpm" @@ -49,7 +49,7 @@ jobs: working-directory: app steps: - name: ⬆️ Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - uses: subosito/flutter-action@v2.23.0 with: flutter-version-file: app/pubspec.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 08f44e0b0723..f10ed2b43201 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: update-changelog: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: token: ${{ secrets.CI_PAT }} fetch-depth: 0 @@ -61,7 +61,7 @@ jobs: version: ${{ steps.setup.outputs.BUTTERFLY_VERSION }} build_number: ${{ steps.setup.outputs.BUTTERFLY_BUILD_NUMBER }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: token: ${{ secrets.CI_PAT }} ref: ${{ github.ref }} @@ -121,7 +121,7 @@ jobs: - update-changelog - release steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: token: ${{ secrets.CI_PAT }} fetch-depth: 0 @@ -166,14 +166,14 @@ jobs: steps: - name: Checkout main if: ${{ github.ref == 'refs/heads/develop' }} - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: token: ${{ secrets.CI_PAT }} fetch-depth: 0 ref: main - name: Checkout develop if: ${{ github.ref == 'refs/heads/main' }} - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: token: ${{ secrets.CI_PAT }} fetch-depth: 0 @@ -214,7 +214,7 @@ jobs: runs-on: ubuntu-24.04 needs: [release] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: token: ${{ secrets.CI_PAT }} - name: Get information diff --git a/.github/workflows/update-screenshots.yml b/.github/workflows/update-screenshots.yml index 1cf9a47adcc7..989931d1a217 100644 --- a/.github/workflows/update-screenshots.yml +++ b/.github/workflows/update-screenshots.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout repo - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Install Linux Flutter dependencies run: | diff --git a/app/pubspec.lock b/app/pubspec.lock index 961fc5c29eca..ede7f92be798 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -1727,4 +1727,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.12.2 <4.0.0" - flutter: "3.44.6" + flutter: "3.44.7" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index d3e33c736b25..5bcb38d33a68 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -17,7 +17,7 @@ version: 2.6.0-beta.3+190 environment: sdk: ">=3.12.2 <4.0.0" - flutter: 3.44.6 + flutter: 3.44.7 dependencies: flutter: From f00bb9dfb9fc7e25e8e1f7e52e2fccbddcc00a99 Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 21 Jul 2026 20:05:05 +0200 Subject: [PATCH 114/117] Fix creating packs from the selection menu, fixes #1178 --- app/lib/api/file_system.dart | 19 +++++ app/lib/dialogs/packs/asset.dart | 81 ++++++++++++++-------- app/lib/dialogs/packs/dialog.dart | 12 ++-- app/test/dialogs/pack_regression_test.dart | 30 ++++++++ app/test/helpers/mocks.dart | 20 ++++++ metadata/en-US/changelogs/190.txt | 3 +- 6 files changed, 129 insertions(+), 36 deletions(-) create mode 100644 app/test/dialogs/pack_regression_test.dart diff --git a/app/lib/api/file_system.dart b/app/lib/api/file_system.dart index 863ea9e2d22d..0a13d0722a85 100644 --- a/app/lib/api/file_system.dart +++ b/app/lib/api/file_system.dart @@ -388,6 +388,25 @@ class ButterflyFileSystem { PackFileSystem buildDefaultPackSystem({bool forceRecreate = false}) => buildPackSystem(settingsCubit.state.getDefaultRemote(), forceRecreate); + Future createPack( + NoteData pack, { + String? name, + ExternalStorage? storage, + }) async { + final fallback = pack.name?.trim().isNotEmpty == true ? pack.name! : 'pack'; + var fileName = name?.trim().isNotEmpty == true ? name! : fallback; + if (!fileName.endsWith('.bfly')) fileName = '$fileName.bfly'; + final fileSystem = buildPackSystem(storage); + await fileSystem.initialize(); + await fileSystem.createFile(fileName, pack); + final files = await fileSystem.getFiles(); + return files + .where((file) => file.pathWithoutLeadingSlash == fileName) + .firstOrNull + ?.path ?? + fileName; + } + Future?> findPack( NamedItem? Function(NoteData) test, [ ExternalStorage? storage, diff --git a/app/lib/dialogs/packs/asset.dart b/app/lib/dialogs/packs/asset.dart index b96147ad03d0..33c69fc4523b 100644 --- a/app/lib/dialogs/packs/asset.dart +++ b/app/lib/dialogs/packs/asset.dart @@ -14,31 +14,67 @@ import 'package:phosphor_flutter/phosphor_flutter.dart'; import '../../bloc/document_bloc.dart'; import 'pack.dart'; -class AssetDialog extends StatelessWidget { +class AssetDialog extends StatefulWidget { final PackAssetLocation? value; final String initialName; const AssetDialog({super.key, this.value, this.initialName = ''}); + @override + State createState() => _AssetDialogState(); +} + +class _AssetDialogState extends State { + late final ButterflyFileSystem _fileSystem; + late final PackFileSystem _packSystem; + late Future>> _packsFuture; + late String _name; + String? _pack; + + @override + void initState() { + super.initState(); + _fileSystem = context.read(); + _packSystem = _fileSystem.buildDefaultPackSystem(); + _packsFuture = _getPacks(); + _pack = widget.value?.namespace; + _name = widget.value?.key ?? widget.initialName; + } + + Future>> _getPacks() => + _packSystem.initialize().then((_) => _packSystem.getFiles()); + + Future _createPack() async { + final pack = await showDialog( + context: context, + builder: (context) => const PackDialog(), + ); + if (pack == null) return; + final createdPath = await _fileSystem.createPack( + pack, + storage: _packSystem.storage, + ); + final packs = await _packSystem.getFiles(); + if (!mounted) return; + setState(() { + _pack = createdPath; + _packsFuture = Future.value(packs); + }); + } + @override Widget build(BuildContext context) { - String? pack = value?.namespace; - String name = value?.key ?? initialName; - final bloc = context.read(); - final packSystem = context - .read() - .buildDefaultPackSystem(); return FutureBuilder>>( - future: packSystem.initialize().then((_) => packSystem.getFiles()), + future: _packsFuture, builder: (context, snapshot) => BlocBuilder( buildWhen: (previous, current) => previous.data != current.data, builder: (context, state) { if (state is! DocumentLoaded) return const SizedBox(); final packs = snapshot.data ?? >[]; - pack ??= packs.firstOrNull?.path; + _pack ??= packs.firstOrNull?.path; return AlertDialog( title: Text( - value == null + widget.value == null ? AppLocalizations.of(context).addAsset : AppLocalizations.of(context).editAsset, ), @@ -62,24 +98,15 @@ class AssetDialog extends StatelessWidget { ), ); }).toList(), - onSelected: (value) { - pack = value; - }, - initialSelection: pack, + onSelected: (value) => _pack = value, + initialSelection: _pack, expandedInsets: const EdgeInsets.all(0), ), ), const SizedBox(width: 8), IconButton( icon: const PhosphorIcon(PhosphorIconsLight.plusCircle), - onPressed: () async { - final pack = await showDialog( - context: context, - builder: (context) => const PackDialog(), - ); - if (pack == null) return; - bloc.add(PackAdded(pack)); - }, + onPressed: _createPack, tooltip: AppLocalizations.of(context).createPack, ), ], @@ -90,10 +117,8 @@ class AssetDialog extends StatelessWidget { labelText: LeapLocalizations.of(context).name, filled: true, ), - initialValue: name, - onChanged: (value) { - name = value; - }, + initialValue: _name, + onChanged: (value) => _name = value, ), ], ), @@ -108,8 +133,8 @@ class AssetDialog extends StatelessWidget { ), ElevatedButton( onPressed: () { - if (pack == null) return; - Navigator.of(context).pop(PackAssetLocation(pack!, name)); + if (_pack == null) return; + Navigator.of(context).pop(PackAssetLocation(_pack!, _name)); }, child: Text(MaterialLocalizations.of(context).okButtonLabel), ), diff --git a/app/lib/dialogs/packs/dialog.dart b/app/lib/dialogs/packs/dialog.dart index 0d8272c55c19..a1873266ae75 100644 --- a/app/lib/dialogs/packs/dialog.dart +++ b/app/lib/dialogs/packs/dialog.dart @@ -242,14 +242,12 @@ class _PacksDialogState extends State ); } - String _normalizePackFileName(String? name, NoteData pack) { - final fallback = pack.name?.trim().isNotEmpty == true ? pack.name! : 'pack'; - final fileName = name?.trim().isNotEmpty == true ? name! : fallback; - return fileName.endsWith('.bfly') ? fileName : '$fileName.bfly'; - } - Future _addPack(NoteData pack, {String? name}) async { - await _packSystem.createFile(_normalizePackFileName(name, pack), pack); + await _fileSystem.createPack( + pack, + name: name, + storage: _packSystem.storage, + ); _refresh(); } diff --git a/app/test/dialogs/pack_regression_test.dart b/app/test/dialogs/pack_regression_test.dart new file mode 100644 index 000000000000..2f76d453ebbe --- /dev/null +++ b/app/test/dialogs/pack_regression_test.dart @@ -0,0 +1,30 @@ +import 'package:butterfly/cubits/settings.dart'; +import 'package:butterfly/models/defaults.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../helpers/mocks.dart'; + +void main() { + test('creating a pack stores it in the global pack filesystem', () async { + final fileSystem = MockButterflyFileSystem(); + final settingsCubit = fileSystem.settingsCubit as MockSettingsCubit; + when( + () => settingsCubit.state, + ).thenReturn(const ButterflySettings(autosave: false)); + final packSystem = fileSystem.buildDefaultPackSystem(); + final pack = DocumentDefaults.createPack().setMetadata( + DocumentDefaults.createMetadata( + type: NoteFileType.pack, + name: 'New Pack', + ), + ); + + final createdPath = await fileSystem.createPack(pack); + final packs = await packSystem.getFiles(); + + expect(createdPath, packs.single.path); + expect(packs.single.pathWithoutLeadingSlash, 'New Pack.bfly'); + }); +} diff --git a/app/test/helpers/mocks.dart b/app/test/helpers/mocks.dart index d6e0b4c22773..bb612eff4212 100644 --- a/app/test/helpers/mocks.dart +++ b/app/test/helpers/mocks.dart @@ -36,6 +36,26 @@ class MockButterflyFileSystem implements ButterflyFileSystem { PackFileSystem buildDefaultPackSystem({bool forceRecreate = false}) => _packFileSystem; + @override + Future createPack( + NoteData pack, { + String? name, + ExternalStorage? storage, + }) async { + final fallback = pack.name?.trim().isNotEmpty == true ? pack.name! : 'pack'; + var fileName = name?.trim().isNotEmpty == true ? name! : fallback; + if (!fileName.endsWith('.bfly')) fileName = '$fileName.bfly'; + final fileSystem = buildPackSystem(storage); + await fileSystem.initialize(); + await fileSystem.createFile(fileName, pack); + final files = await fileSystem.getFiles(); + return files + .where((file) => file.pathWithoutLeadingSlash == fileName) + .firstOrNull + ?.path ?? + fileName; + } + @override TemplateFileSystem buildDefaultTemplateSystem({bool forceRecreate = false}) => _templateFileSystem; diff --git a/metadata/en-US/changelogs/190.txt b/metadata/en-US/changelogs/190.txt index 614c5b121aca..5e39a1505174 100644 --- a/metadata/en-US/changelogs/190.txt +++ b/metadata/en-US/changelogs/190.txt @@ -10,5 +10,6 @@ * Fix file previews resetting the language to the system locale * Fix filename preview appearing when renaming existing documents * Fix navigation menus broken on mobile layout ([#1177](https://github.com/LinwoodDev/Butterfly/issues/1177)) +* Fix creating packs from the selection menu ([#1178](https://github.com/LinwoodDev/Butterfly/issues/1178)) -Read more here: https://linwood.dev/butterfly/2.6.0-beta.3 \ No newline at end of file +Read more here: https://linwood.dev/butterfly/2.6.0-beta.3 From 61d29d18c91e7622ea8eef046ab3556a3c6620db Mon Sep 17 00:00:00 2001 From: CodeDoctorDE Date: Tue, 21 Jul 2026 20:05:25 +0200 Subject: [PATCH 115/117] Fix moving collection elements to layers, fixes #1176 --- app/lib/dialogs/collections.dart | 2 +- .../dialogs/collection_regression_test.dart | 92 +++++++++++++++++++ metadata/en-US/changelogs/190.txt | 1 + 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 app/test/dialogs/collection_regression_test.dart diff --git a/app/lib/dialogs/collections.dart b/app/lib/dialogs/collections.dart index 317e42dae1aa..0f83fbd730b7 100644 --- a/app/lib/dialogs/collections.dart +++ b/app/lib/dialogs/collections.dart @@ -163,7 +163,7 @@ class _CollectionsDialogState extends State { .map((e) => e.id) .nonNulls .toList(); - showDialog( + await showDialog( builder: (context) => BlocProvider.value( value: bloc, child: MoveToLayerDialog(elementIds: elementIds), diff --git a/app/test/dialogs/collection_regression_test.dart b/app/test/dialogs/collection_regression_test.dart new file mode 100644 index 000000000000..2a685ca32e4c --- /dev/null +++ b/app/test/dialogs/collection_regression_test.dart @@ -0,0 +1,92 @@ +import 'package:archive/archive.dart'; +import 'package:butterfly/api/file_system.dart'; +import 'package:butterfly/bloc/document_bloc.dart'; +import 'package:butterfly/cubits/editor_controller.dart'; +import 'package:butterfly/cubits/settings.dart'; +import 'package:butterfly/cubits/transform.dart'; +import 'package:butterfly/dialogs/collections.dart'; +import 'package:butterfly/dialogs/layers.dart'; +import 'package:butterfly/models/viewport.dart'; +import 'package:butterfly/src/generated/i18n/app_localizations.dart'; +import 'package:butterfly_api/butterfly_api.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lw_file_system/lw_file_system.dart'; +import 'package:material_leap/material_leap.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../helpers/mocks.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('move to layer keeps the layer picker open', (tester) async { + final fileSystem = MockButterflyFileSystem(); + final settingsCubit = fileSystem.settingsCubit as MockSettingsCubit; + when( + () => settingsCubit.state, + ).thenReturn(const ButterflySettings(autosave: false)); + when(() => settingsCubit.stream).thenAnswer((_) => const Stream.empty()); + + final editorController = EditorController( + settingsCubit, + TransformCubit(1), + CameraViewport.unbaked(), + ); + final windowCubit = WindowCubit(fullScreen: false); + final element = PenElement(id: 'element', collection: 'collection'); + final page = DocumentPage( + layers: [ + DocumentLayer(id: 'bottom', name: 'Bottom'), + DocumentLayer(id: 'top', name: 'Top', content: [element]), + ], + ); + final (data, pageName) = NoteData(Archive()).setPage(page, 'Page'); + final bloc = DocumentBloc( + fileSystem, + editorController, + windowCubit, + data, + const AssetLocation(path: 'regression-test.bfly'), + null, + page, + pageName, + ); + addTearDown(() async { + await bloc.close(); + await editorController.close(); + await windowCubit.close(); + }); + + await tester.pumpWidget( + RepositoryProvider.value( + value: fileSystem, + child: BlocProvider.value( + value: bloc, + child: MaterialApp( + locale: const Locale('en'), + localizationsDelegates: const [ + ...AppLocalizations.localizationsDelegates, + LeapLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: const Scaffold(body: CollectionsDialog()), + ), + ), + ), + ); + bloc.add(const CurrentCollectionChanged('collection')); + await tester.pumpAndSettle(); + + await tester.tap(find.byTooltip('Move to layer')); + await tester.pumpAndSettle(); + + expect(find.byType(MoveToLayerDialog), findsOneWidget); + await tester.tap(find.text('Bottom')); + await tester.pumpAndSettle(); + + final state = bloc.state as DocumentLoadSuccess; + expect(state.page.getLayer('bottom').content.single.id, 'element'); + }); +} diff --git a/metadata/en-US/changelogs/190.txt b/metadata/en-US/changelogs/190.txt index 5e39a1505174..21bc59e597fb 100644 --- a/metadata/en-US/changelogs/190.txt +++ b/metadata/en-US/changelogs/190.txt @@ -11,5 +11,6 @@ * Fix filename preview appearing when renaming existing documents * Fix navigation menus broken on mobile layout ([#1177](https://github.com/LinwoodDev/Butterfly/issues/1177)) * Fix creating packs from the selection menu ([#1178](https://github.com/LinwoodDev/Butterfly/issues/1178)) +* Fix moving collection elements to layers ([#1176](https://github.com/LinwoodDev/Butterfly/issues/1176)) Read more here: https://linwood.dev/butterfly/2.6.0-beta.3 From a6d7d71fbf849ca5acf08f0c6befb515fb621ac6 Mon Sep 17 00:00:00 2001 From: Nezznee Date: Sun, 26 Jul 2026 00:46:30 +0200 Subject: [PATCH 116/117] Improve documentation, add element context menu page and paint options page --- docs/astro.config.mjs | 12 +++-- docs/src/content/docs/docs/v2/add.md | 2 +- .../src/content/docs/docs/v2/context_menu.mdx | 48 +++++++++++++++++ docs/src/content/docs/docs/v2/layers.md | 1 + docs/src/content/docs/docs/v2/migrating.md | 2 +- .../src/content/docs/docs/v2/paint_options.md | 51 ++++++++++++++++++ docs/src/content/docs/docs/v2/shortcuts.md | 2 +- docs/src/content/docs/docs/v2/storage.md | 4 +- docs/src/content/docs/docs/v2/tools/area.md | 14 ++--- .../src/content/docs/docs/v2/tools/barcode.md | 16 +++--- .../content/docs/docs/v2/tools/collection.md | 14 ----- .../content/docs/docs/v2/tools/collection.mdx | 24 +++++++++ docs/src/content/docs/docs/v2/tools/eraser.md | 21 ++++++-- .../content/docs/docs/v2/tools/full_screen.md | 4 +- docs/src/content/docs/docs/v2/tools/grid.md | 19 +++---- docs/src/content/docs/docs/v2/tools/label.md | 48 ++++++++--------- docs/src/content/docs/docs/v2/tools/laser.md | 22 ++++---- .../content/docs/docs/v2/tools/path_eraser.md | 14 ----- docs/src/content/docs/docs/v2/tools/pen.md | 30 ++++++----- .../src/content/docs/docs/v2/tools/polygon.md | 40 -------------- .../content/docs/docs/v2/tools/polygon.mdx | 41 ++++++++++++++ docs/src/content/docs/docs/v2/tools/redo.md | 4 +- docs/src/content/docs/docs/v2/tools/select.md | 20 +++++-- docs/src/content/docs/docs/v2/tools/shape.md | 53 ++++++------------- .../src/content/docs/docs/v2/tools/texture.md | 21 +++----- docs/src/content/docs/docs/v2/tools/undo.md | 4 +- docs/src/translations/en.json | 5 +- 27 files changed, 321 insertions(+), 215 deletions(-) create mode 100644 docs/src/content/docs/docs/v2/context_menu.mdx create mode 100644 docs/src/content/docs/docs/v2/paint_options.md delete mode 100644 docs/src/content/docs/docs/v2/tools/collection.md create mode 100644 docs/src/content/docs/docs/v2/tools/collection.mdx delete mode 100644 docs/src/content/docs/docs/v2/tools/path_eraser.md delete mode 100644 docs/src/content/docs/docs/v2/tools/polygon.md create mode 100644 docs/src/content/docs/docs/v2/tools/polygon.mdx diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 8945376c7e10..3d318ad8d1dd 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -69,6 +69,10 @@ export default defineConfig({ ...getSidebarTranslatedLabel("Introduction"), link: "/docs/v2/intro", }, + { + ...getSidebarTranslatedLabel("Context menu"), + link: "/docs/v2/context_menu", + }, { ...getSidebarTranslatedLabel("Area"), link: "/docs/v2/areas/", @@ -81,6 +85,10 @@ export default defineConfig({ ...getSidebarTranslatedLabel("Color picker"), link: "/docs/v2/color_picker/", }, + { + ...getSidebarTranslatedLabel("Paint Options"), + link: "/docs/v2/paint_options/", + }, { ...getSidebarTranslatedLabel("Layers"), link: "/docs/v2/layers/", @@ -152,10 +160,6 @@ export default defineConfig({ ...getSidebarTranslatedLabel("Hand"), link: "/docs/v2/tools/hand/", }, - { - ...getSidebarTranslatedLabel("Path eraser"), - link: "/docs/v2/tools/path_eraser/", - }, { ...getSidebarTranslatedLabel("Eraser"), link: "/docs/v2/tools/eraser/", diff --git a/docs/src/content/docs/docs/v2/add.md b/docs/src/content/docs/docs/v2/add.md index ed7b07d281a8..1f62ee3dfc8d 100644 --- a/docs/src/content/docs/docs/v2/add.md +++ b/docs/src/content/docs/docs/v2/add.md @@ -7,7 +7,7 @@ Here you can add things to your notes. ## Import Here you can import existing files into your documents. -Supported are Butterfly documents, Markdown, PNG, SVG, and PDF. +Supported are Butterfly, Xournal++ and OneNote documents, Markdown, PNG, SVG, and PDF. ## Tools diff --git a/docs/src/content/docs/docs/v2/context_menu.mdx b/docs/src/content/docs/docs/v2/context_menu.mdx new file mode 100644 index 000000000000..b95a48c7e20f --- /dev/null +++ b/docs/src/content/docs/docs/v2/context_menu.mdx @@ -0,0 +1,48 @@ +--- +title: Context Menu +--- +import {Scissors, + Copy, + CopySimple, + Trash, + Layout, + Wrench, + Faders, + Folder, + Stack, + Export, + PlusCircle, + Polygon, + ArrowUpRight, + +} from "@phosphor-icons/react"; + +To access the Context menu right click on a selection. + +## Actions +The Context menu contains actions that can be executed on the selected elements. Selections with only one element may have additional action specific to that element. + +* `Cut` - Deletes the selection and adds it to the clipboard. +* `Copy` - Adds the selection it to the clipboard. +* `Duplicate` - Creates a duplicate version of the selection to place on the canvas. +* `Delete` - Deletes the selection. +* `Arrange` - [Options](#arrange-options) that change the order in which the selection is rendered relative to the other elements in it's [Layer](../layers). +* `Operations` - Flips the selection vertically or horizontally. +* `Properties` - Opens the properties view of the selection. Default properties are position, rotation and shear +* `Change collection` - Changes the [Collection](../tools/collection) this selection belongs to. Here you need to enter the name of the (maybe nonexistent) collection. +* `Move to layer` - Changes the [Layer](../layers) this selection belongs to. +* `Export` - Exports the selection as an Image, SVG or PDF. +* `Add to pack` - Adds the selection as an asset to a [Pack](../pack/#adding-a-component-into-a-pack) ! + +Element specific actions: +* `Edit` - Enters edit mode for this specific [Polygon](../tools/polygon) + +### Arrange options + +| Option | Description | +|---------------:|:------------------------------------------------------------------| +| Bring forward | Renders the selection on top of the first element in front of it. | +| Send backward | Renders the selection under the first element behind in. | +| Bring to front | Renders the selection in front of everything else. | +| Send backward | Renders the selection behind everything else. | + diff --git a/docs/src/content/docs/docs/v2/layers.md b/docs/src/content/docs/docs/v2/layers.md index 1f203ea47c3c..94ce488cdd4b 100644 --- a/docs/src/content/docs/docs/v2/layers.md +++ b/docs/src/content/docs/docs/v2/layers.md @@ -10,6 +10,7 @@ Layers are drawn from bottom to top, so the top layer will be shown above all ot Be aware that [Collections](../tools/collection) are not the same as layers. Collections are a lightweight way to group elements, but do not affect the order in which they are rendered. +For changing the rendering order within a layer see [Arrange](../context_menu/#arrange-options) ::: ## The Layers dialog diff --git a/docs/src/content/docs/docs/v2/migrating.md b/docs/src/content/docs/docs/v2/migrating.md index 2a4883a69d42..4a891140c80a 100644 --- a/docs/src/content/docs/docs/v2/migrating.md +++ b/docs/src/content/docs/docs/v2/migrating.md @@ -4,7 +4,7 @@ title: Migrating This page lists breaking changes that may affect you when updating to newer versions. -## Version 2.0 (File Version 7) {#7} +## Version 2.0 (File Version 7) The eraser layer has been removed. Upon updating to version 2.0, the eraser layer will be removed automatically. diff --git a/docs/src/content/docs/docs/v2/paint_options.md b/docs/src/content/docs/docs/v2/paint_options.md new file mode 100644 index 000000000000..92106e84a08b --- /dev/null +++ b/docs/src/content/docs/docs/v2/paint_options.md @@ -0,0 +1,51 @@ +--- +title: Paint Options +--- + +For some [surface tools](../add/#surfaces) like the [Pen](../tools/pen/), [Shape](../tools/shape/) +and [Polygon](../tools/polygon) tools, you can configure how the paint of the tool behaves. This +section will cover the different Paint modes for both the **color** and the **fill** properties. + +## Solid Color + +Normal Strokes + +| Property | Default | Description | +|---------:|:-------------------:|:----------------------------------------------------------------| +| Color | Black / Transparent | The color of the property | +| Alpha | 255 / 0 | The opacity of the color | +| Blur | 0 | How much the color transition to the background will be blurred | + +## Image, SVG + +Draw the imported image with your strokes + +| Property | Default | Description | +|---------:|:-------:|:------------------------------------------------------------------------| +| Tint | Black | The color filter; Black makes the image invisible, White uses no filter | +| Alpha | 255 / 0 | The opacity of the color | +| Blur | 0 | How much the color transition to the background will be blurred | + +## Gradient + +Make your strokes transition between colors + +### Linear Gradient + +| Property | Default | Description | +|------------:|:-------------:|:-----------------------------------------------------------------------------------------------------------------------------------| +| Start | (0,0) | Starting position of the linear-gradient axis; percentually relative to the top left corner | +| End | (1,0) | End position of the linear-gradient axis; percentually relative to the top left corner | +| Color stops | 2 Color stops | The offset of a Color stop percentually defines where along the gradient axis a color is placed. There are color and alpha options | + +### Radial Gradient + +| Property | Default | Description | +|--------------:|:--------------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------| +| Center | (0.5,0.5) | Geometric middle of the boundary circle at 100% offset; percentually relative to the top left corner | +| Center radius | 0.5 | Radius of the boundary circle; percentually relative to the top left corner | +| Focal Point | off (same as center) | Where the 0% offset position is placed. If it's not same as center then a ellipse instead of a circle will form. | +| Focal radius | 0 | Radius of the inner circle where the offset is at 0%. Behind it the offset increases. | +| Color stops | 2 Color stops | The offset of a Color stop percentually defines where between focal point and the boundary the color is placed. There are color and alpha options | + + diff --git a/docs/src/content/docs/docs/v2/shortcuts.md b/docs/src/content/docs/docs/v2/shortcuts.md index cae71a605adc..5518d97b9cf8 100644 --- a/docs/src/content/docs/docs/v2/shortcuts.md +++ b/docs/src/content/docs/docs/v2/shortcuts.md @@ -41,7 +41,7 @@ By default, the pen is configured to function as the following: * `First` (Primary button, if supported): Change to hand tool while pressed. * `Second` (Secondary button, if supported): Change to second tool (see [configure](#configure) section below) while pressed. -## Configuring {#configure} +## Configuring You can customize your controls by changing which tools your inputs map to. diff --git a/docs/src/content/docs/docs/v2/storage.md b/docs/src/content/docs/docs/v2/storage.md index 1eba63fcfc1b..40ed2df982c3 100644 --- a/docs/src/content/docs/docs/v2/storage.md +++ b/docs/src/content/docs/docs/v2/storage.md @@ -25,7 +25,7 @@ Open the developer tools in your browser and you will see the data. By default, the application saves the data in your documents folder in a subfolder called "Linwood/Butterfly". This folder is created when you save data for the first time. This folder can be changed in the settings. -## Remote storage {#remote} +## Remote storage :::note @@ -42,7 +42,7 @@ To get the WebDAV URL, please visit the documentation: * [Nextcloud](https://docs.nextcloud.com/server/latest/user_manual/en/files/access_webdav.html) (it should look like this: `https://nextcloud.example.com/remote.php/dav/files/username/`, replace `username` and `nextcloud.example.com` with the correct values) -### Offline sync {#offline} +### Offline sync This feature allows you to edit your files on remote servers while you are offline. Open the popup menu on a file or folder and click on `Sync`. This will download the file or folder and save it locally. To sync the whole root directory, click on the checkmark in the create dialog or click on the remote in the setting and click on the checkmark in the manage section. diff --git a/docs/src/content/docs/docs/v2/tools/area.md b/docs/src/content/docs/docs/v2/tools/area.md index fd37d0cc6898..55df3d53dd58 100644 --- a/docs/src/content/docs/docs/v2/tools/area.md +++ b/docs/src/content/docs/docs/v2/tools/area.md @@ -9,18 +9,20 @@ For an overview of how areas work, see [Areas](../../areas). ## Actions | Mouse | Touch | Action | -| :-----------------: | :----------: | :---------------: | +|:-------------------:|:------------:|:-----------------:| | Left click and drag | Tap and drag | Create a new area | | Middle click | Two fingers | Move canvas | | Right click | Long tap | Edit area | ## Configuration -| Property | Default | Description | -| -----------: | :-----: | :----------------------------------------------------------------------------------------------------------------------------------------------- | -| Width | `0` | The fixed width for new areas. If set to `0`, this setting will be ignored. | -| Height | `0` | The fixed height for new areas. If set to `0`, this setting will be ignored. | -| Aspect ratio | `0` | The fixed aspect ratio for new areas. Press the button to access some common presets. An aspect ratio is defined as width / height, so values less than `1` will be taller than they are wide, and values greater than `1` will be wider than they are tall. If set to `0`, this setting will be ignored. | +| Property | Default | Description | +|-------------:|:-------:|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Ask for name | false | Whether a name prompt will appear before creation. | +| Presets | none | Various presets for width and heigth. | +| Width | `0` | The fixed width for new areas. If set to `0`, this setting will be ignored. | +| Height | `0` | The fixed height for new areas. If set to `0`, this setting will be ignored. | +| Aspect ratio | `0` | The fixed aspect ratio for new areas. Press the button to access some common presets. An aspect ratio is defined as width / height, so values less than `1` will be taller than they are wide, and values greater than `1` will be wider than they are tall. If set to `0`, this setting will be ignored. | There are three aspect ratio presets: diff --git a/docs/src/content/docs/docs/v2/tools/barcode.md b/docs/src/content/docs/docs/v2/tools/barcode.md index 20194fafddec..d383ba4889f2 100644 --- a/docs/src/content/docs/docs/v2/tools/barcode.md +++ b/docs/src/content/docs/docs/v2/tools/barcode.md @@ -16,15 +16,15 @@ The generated barcode is added as an element on the canvas. You can move, resize ## Barcode types -| Type | Description | -| ---: | :---------- | -| QR Code | A square 2D code that is commonly used for links, short text, and contact information. | -| Data Matrix | A compact 2D code that is useful when the barcode should stay small. | -| Code 128 | A 1D barcode for alphanumeric data. | +| Type | Description | +|------------:|:---------------------------------------------------------------------------------------| +| QR Code | A square 2D code that is commonly used for links, short text, and contact information. | +| Data Matrix | A compact 2D code that is useful when the barcode should stay small. | +| Code 128 | A 1D barcode for alphanumeric data. | ## Configuration -| Property | Default | Description | -| -------: | :-----: | :---------- | +| Property | Default | Description | +|-------------:|:-------:|:------------------------------------------| | Barcode type | QR Code | The type of barcode that will be created. | -| Color | Black | The color of the generated barcode. | +| Color | Black | The color of the generated barcode. | diff --git a/docs/src/content/docs/docs/v2/tools/collection.md b/docs/src/content/docs/docs/v2/tools/collection.md deleted file mode 100644 index 391210b26987..000000000000 --- a/docs/src/content/docs/docs/v2/tools/collection.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Collection tool ---- - -:::note[⚡ Action tool] - -This is a special tool. -You can't select it and it will run the action if you click on it. - -::: - -With this tool group multiple elements together. For more complex grouping see [layers](../../layers). - -When you click on an object, the collection of the object will be changed to the collection set in the configuration. Leaving the collection field empty will set the collection to the default collection. diff --git a/docs/src/content/docs/docs/v2/tools/collection.mdx b/docs/src/content/docs/docs/v2/tools/collection.mdx new file mode 100644 index 000000000000..20f087848b40 --- /dev/null +++ b/docs/src/content/docs/docs/v2/tools/collection.mdx @@ -0,0 +1,24 @@ +--- +title: Collection tool +--- +import {Cursor, Selection, TextT, Trash, Stack} from "@phosphor-icons/react"; + + +:::note[⚡ Action tool] + +This is a special action tool. +It can't be selected and will run the action immediately when you click on it. + +::: + +With this tool group multiple elements together. For more complex grouping see [layers](../../layers). + +When you create an object, it's collection will be set to the active collection set in the configuration. To change the collection of an existing object click "Change collection" and then proceed to type the name of the collection (which may or may not already exist). Leaving the collection field empty will set the collection to the default collection. + +To select the active collection click on it's name in the collection tool menu. There can only be one empty collection at once, and it will vanish as soon as another collection is selected. + +* `Select a custom collection` - Create a new empty collection and makes it the active collection. +* `Select` - Selects all elements of the active collection. +* `Rename` - Changes the name of the active collection. The default collection cannot be renamed. +* `Delete elements` - Deletes the active collection along with all of its elements. +* `Move to Layer` - Moves all elements in the active collection to a specific layer. diff --git a/docs/src/content/docs/docs/v2/tools/eraser.md b/docs/src/content/docs/docs/v2/tools/eraser.md index fd5d7d667cac..dd6d5572fb4f 100644 --- a/docs/src/content/docs/docs/v2/tools/eraser.md +++ b/docs/src/content/docs/docs/v2/tools/eraser.md @@ -6,7 +6,20 @@ With this tool you can erase the elements on the paper. ## Configuration -| Property | Default | Description | -|------------------:|:-------:|:---------------------------------------------------------------------------------| -| Stroke width | 5 | The width of the stroke | -| Stroke multiplier | 1 | If you have a stylus, this is the multiplier which will be added to the pressure | +| Property | Default | Description | +|-------------------:|:--------------:|:---------------------------------------------------------------------------------------------------------| +| Mode | Path | Eraser mode. Valid are Stroke and Path | +| Stroke width | 5 | The width of the stroke | +| Erase shapes | Touch anywhere | [Erase shapes mode](#erase-shapes-mode) | +| Erase all elements | false | Enables erasure for all elements (e.g. images and barcodes), except for those discussed in Erase shapes. | + +### Erase shapes mode + +Defines which part of a [shape](../shape) or [polygon](../polygon) needs to be touched for it to be +erased. + +| Mode | Description | +|-----------------------------:|:----------------------------------------------------------------------------| +| Don't erase shapes | The eraser will not interact with shapes. | +| Erase when touching edges | The shape will be erased when the eraser touches any edge. | +| Erase when touching anywhere | The shape will be erased when the eraser in used anywhere inside the shape. | \ No newline at end of file diff --git a/docs/src/content/docs/docs/v2/tools/full_screen.md b/docs/src/content/docs/docs/v2/tools/full_screen.md index ea23ff72ac89..6e9d12d34f61 100644 --- a/docs/src/content/docs/docs/v2/tools/full_screen.md +++ b/docs/src/content/docs/docs/v2/tools/full_screen.md @@ -2,10 +2,10 @@ title: Full screen tool --- -:::note[⚡ Action tool] +:::note[🔘 Toggleable tool] This is a special tool. -You can't select it and it will run the action if you click on it. +It can't be selected and will run the action immediately when you click on it. ::: diff --git a/docs/src/content/docs/docs/v2/tools/grid.md b/docs/src/content/docs/docs/v2/tools/grid.md index 676859a9ac03..178d40899243 100644 --- a/docs/src/content/docs/docs/v2/tools/grid.md +++ b/docs/src/content/docs/docs/v2/tools/grid.md @@ -5,7 +5,7 @@ title: Grid tool :::note[🔘 Toggleable tool] This is a special tool. -You can't select it and it gets toggled if you click on it. +It can't be selected and will run the action immediately when you click on it. ::: @@ -14,11 +14,12 @@ Inputs get snapped to the grid. ## Configuration -| Property | Default | Description | -| -------: | :------: | :---------- | -| Size | (20, 20) | The size of the grid cells on the x and y axis. | -| Offset | (0, 0) | The offset of the grid on the x and y axis. | -| Color | Black | The color of the grid. | -| Stroke | 1 | The width of the grid lines. | -| Zoom dependent | false | Changes the grid stroke width based on the zoom level. | -| Position dependent | false | Makes the grid depend on the canvas position instead of staying fixed on the viewport. | +| Property | Default | Description | +|-------------------:|:--------:|:---------------------------------------------------------------------------------------| +| Size | (20, 20) | The size of the grid cells on the x and y axis. | +| Offset | (0, 0) | The offset of the grid on the x and y axis. | +| Color | Black | The color of the grid. | +| Alpha | 255 | The opacity of the grid liens. | +| Stroke | 1 | The width of the grid lines. | +| Zoom dependent | false | Changes the grid stroke width based on the zoom level. | +| Position dependent | false | Makes the grid depend on the canvas position instead of staying fixed on the viewport. | \ No newline at end of file diff --git a/docs/src/content/docs/docs/v2/tools/label.md b/docs/src/content/docs/docs/v2/tools/label.md index 4ef9565b623a..322900d55957 100644 --- a/docs/src/content/docs/docs/v2/tools/label.md +++ b/docs/src/content/docs/docs/v2/tools/label.md @@ -8,9 +8,9 @@ With this tool you can add text, Markdown, or mathematical formulas into the inf The label tool can be used in different modes: -| Mode | Description | -| ---: | :---------- | -| Text | Adds plain text labels. | +| Mode | Description | +|---------:|:--------------------------------| +| Text | Adds plain text labels. | | Markdown | Adds formatted Markdown labels. | ## Mathematics @@ -47,27 +47,27 @@ $$ ### Useful commands -| Command | Description | Example | Example command | -| ------- | ----------- | ------- | --------------- | -| `\sqrt[n]{arg}` | Square root symbol, or nth root | $\sqrt[3]{x+1}$ | `\sqrt[3]{x+1}` | -| `\frac{num}{den}` | Fraction with numerator and denominator | $\frac{a+1}{b-1}$ | `\frac{a+1}{b-1}` | -| `\stackrel{a}{b}` | Places something (`a`) above another (`b`) | $\stackrel{!}{=}$ | `\stackrel{!}{=}` | -| `\left` and `\right` | Scaling delimiters. `\left` must be paired with a `\right` | $\left( \frac{x}{2} \right)$ | `\left( \frac{x}{2} \right)` | -| `\sum_{lower}^{upper}` | Summation symbol with limits | $\sum_{i=1}^{n} i$ | `\sum_{i=1}^{n} i` | -| `\mid` | Vertical bar as relation, such as “divides” or conditional | $a\mid b$ | `a\mid b` | -| `\prod_{lower}^{upper}` | Product symbol with limits | $\prod_{k=1}^{m} k$ | `\prod_{k=1}^{m} k` | -| `\int_{a}^{b}` | Integral with limits | $\int_{0}^{1} x^2\,dx$ | `\int_{0}^{1} x^2\,dx` | -| `\langle` and `\rangle` | Angle brackets for inner products or tuples | $\langle v,w\rangle$ | `\langle v,w\rangle` | -| `\in` and `\notin` | Set membership or not membership | $x\in A$, $y\notin B$ | `x\in A`, `y\notin B` | -| `\forall` and `\exists` | Universal or existential quantifiers | $\forall x\in\mathbb{R},\ \exists y$ | `\forall x\in\mathbb{R},\ \exists y` | -| `\to` | Right arrow for functions or limits | $f:A\to B$, $x_n\to x$ | `f:A\to B`, `x_n\to x` | +| Command | Description | Example | Example command | +|--------------------------------------------|------------------------------------------------------------|--------------------------------------|--------------------------------------| +| `\sqrt[n]{arg}` | Square root symbol, or nth root | $\sqrt[3]{x+1}$ | `\sqrt[3]{x+1}` | +| `\frac{num}{den}` | Fraction with numerator and denominator | $\frac{a+1}{b-1}$ | `\frac{a+1}{b-1}` | +| `\stackrel{a}{b}` | Places something (`a`) above another (`b`) | $\stackrel{!}{=}$ | `\stackrel{!}{=}` | +| `\left` and `\right` | Scaling delimiters. `\left` must be paired with a `\right` | $\left( \frac{x}{2} \right)$ | `\left( \frac{x}{2} \right)` | +| `\sum_{lower}^{upper}` | Summation symbol with limits | $\sum_{i=1}^{n} i$ | `\sum_{i=1}^{n} i` | +| `\mid` | Vertical bar as relation, such as “divides” or conditional | $a\mid b$ | `a\mid b` | +| `\prod_{lower}^{upper}` | Product symbol with limits | $\prod_{k=1}^{m} k$ | `\prod_{k=1}^{m} k` | +| `\int_{a}^{b}` | Integral with limits | $\int_{0}^{1} x^2\,dx$ | `\int_{0}^{1} x^2\,dx` | +| `\langle` and `\rangle` | Angle brackets for inner products or tuples | $\langle v,w\rangle$ | `\langle v,w\rangle` | +| `\in` and `\notin` | Set membership or not membership | $x\in A$, $y\notin B$ | `x\in A`, `y\notin B` | +| `\forall` and `\exists` | Universal or existential quantifiers | $\forall x\in\mathbb{R},\ \exists y$ | `\forall x\in\mathbb{R},\ \exists y` | +| `\to` | Right arrow for functions or limits | $f:A\to B$, $x_n\to x$ | `f:A\to B`, `x_n\to x` | ## Configuration -| Property | Default | Description | -| -------: | :-----: | :---------- | -| Mode | Text | The label mode. Available modes are Text and Markdown. | -| Foreground | Black | The text color. | -| Scale | 2 | The scale of the label. | -| Zoom dependent | false | Changes the label size based on the zoom level. | -| Style sheet | None | The style sheet used for the label. | +| Property | Default | Description | +|---------------:|:-------:|:-------------------------------------------------------| +| Mode | Text | The label mode. Available modes are Text and Markdown. | +| Foreground | Black | The text color. | +| Scale | 2 | The scale of the label. | +| Zoom dependent | false | Changes the label size based on the zoom level. | +| Style sheet | None | The style sheet used for the label. | diff --git a/docs/src/content/docs/docs/v2/tools/laser.md b/docs/src/content/docs/docs/v2/tools/laser.md index c96434b087b5..2ba735e862a2 100644 --- a/docs/src/content/docs/docs/v2/tools/laser.md +++ b/docs/src/content/docs/docs/v2/tools/laser.md @@ -7,18 +7,18 @@ Add a new drawing to the paper to cancel the previous laser stroke. ## Configuration -| Property | Default | Description | -| -------: | :-----: | :---------- | -| Color | Red | The color that will be drawn. | -| Stroke width | 5 | The width of the stroke. | -| Thinning | 0.4 | The effect of pressure on the stroke size. Set it to `0` for a constant stroke width. | -| Duration | 5 | The duration in seconds that the drawing will stay visible. | -| Hide duration | 0.5 | How long the laser stroke takes to disappear. | -| Animation | Fade | The animation used when the laser stroke disappears. | +| Property | Default | Description | +|--------------:|:-------:|:--------------------------------------------------------------------------------------| +| Color | Red | The color that will be drawn. | +| Stroke width | 5 | The width of the stroke. | +| Thinning | 0.4 | The effect of pressure on the stroke size. Set it to `0` for a constant stroke width. | +| Duration | 5 | The duration in seconds that the drawing will stay visible. | +| Hide duration | 0.5 | How long the laser stroke takes to disappear. | +| Animation | Fade | The animation used when the laser stroke disappears. | ## Animation modes -| Mode | Description | -| ---: | :---------- | +| Mode | Description | +|-----:|:----------------------------------------------------| | Fade | The stroke fades out after the duration has passed. | -| Path | The stroke disappears along the drawn path. | +| Path | The stroke disappears along the drawn path. | diff --git a/docs/src/content/docs/docs/v2/tools/path_eraser.md b/docs/src/content/docs/docs/v2/tools/path_eraser.md deleted file mode 100644 index 3c475283f3eb..000000000000 --- a/docs/src/content/docs/docs/v2/tools/path_eraser.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Path eraser tool ---- - - -With this tool you can delete the whole path on the paper. - -## Configuration - -| Property | Default | Description | -|------------------:|:-------:|:----------------------------------------------------------------------------------------------------------------| -| Stroke width | 5 | The width of the stroke | -| Stroke multiplier | 1 | If you have a stylus, this is the multiplier which will be added to the pressure | -| Include eraser | false | This setting controls if you can remove the path of the eraser. The path under the removed eraser will be shown | diff --git a/docs/src/content/docs/docs/v2/tools/pen.md b/docs/src/content/docs/docs/v2/tools/pen.md index bb21a0813f05..263054ca0fce 100644 --- a/docs/src/content/docs/docs/v2/tools/pen.md +++ b/docs/src/content/docs/docs/v2/tools/pen.md @@ -20,20 +20,22 @@ First, add a new pen tool into the toolbar 1. Press the + button in the toolbar! 2. Add a pen 3. Hold the new pen icon to move it - - Note: when moving any tool, make sure to modify the settings in the behaviors tab to make sure the correct tool is activated for each input + - Note: when moving any tool, make sure to modify the settings in the behaviors tab to make sure + the correct tool is activated for each input Then, modify the new pen 1. Open the properties panel of the new pen by pressing it again 2. Rename the pen by double tapping the tool's name. -3. After renaming the pen to highlighter, change the icon to match the highlighter look by pressing the icon +3. After renaming the pen to highlighter, change the icon to match the highlighter look by pressing + the icon Finally, after modifying the tool's appearance. It's time to modify the properties! 1. Change the stroke width to a big number, for example, 50. Highlighters have big strokes after all 2. Set thinning to 0. - Highlighters don't have variable stroke width. + Highlighters don't have variable stroke width. 3. Also set the smoothing to 0. Smoothing is something that highlighters never make. 4. Set the streamline to MAX (1) @@ -52,14 +54,14 @@ Finally, after modifying the tool's appearance. It's time to modify the properti ## Configuration -| Property | Default | Description | -| --------------: | :-----------------: | :------------------------------------------------------------------------------------------------------ | -| Color | Black | The color that will be drawn | -| Stroke width | 5 | The width of the stroke | -| Zoom dependent | false | This will change the stroke width based on the zoom level. | -| Shape Detection | false (Delay: 0.5s) | This will try to detect shapes while drawing. If a shape is detected, it will be replaced by the shape. | -| Thinning | 0.4 | This effect of pressure on the stroke size | -| Smoothing | 0.5 | This will smooth the edges of the stroke. | -| Streamline | 0.5 | How much the pen will follow the movement of the mouse. | -| Color | Black | The color that will be drawn | -| Fill | Transparent | The color that will be drawn inside the shape | +| Property | Default | Description | +|----------------:|:-------------------------:|:--------------------------------------------------------------------------------------------------------| +| Zoom dependent | false | This will change the stroke width based on the zoom level. | +| Combine paths | false | This will merge all strokes it touches into one path | +| Shape Detection | false (Delay: 0.5s) | This will try to detect shapes while drawing. If a shape is detected, it will be replaced by the shape. | +| Stroke width | 5 | The width of the stroke | +| Thinning | 0.4 | This effect of pressure on the stroke size | +| Smoothing | 0.5 | This will smooth the edges of the stroke. | +| Streamline | 0.5 | How much the pen will follow the movement of the mouse. | +| Color | Solid Color (Black) | [Paint options](../../paint_options/) for the color of the stroke | +| Fill | Solid Color (Transparent) | [Paint options](../../paint_options/) for the color drawn inside the enclosed area | \ No newline at end of file diff --git a/docs/src/content/docs/docs/v2/tools/polygon.md b/docs/src/content/docs/docs/v2/tools/polygon.md deleted file mode 100644 index 0d364e7a74ec..000000000000 --- a/docs/src/content/docs/docs/v2/tools/polygon.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: Polygon tool ---- - -With this tool you can create custom shapes. - -## Usage - -1. Click on the canvas to add the first point of the polygon. -2. Click again to add subsequent points. -3. Now you have three options to finish the polygon: - 1. Click in the toolbar on the `Finish` button. This will connect the last point to the first point. - 2. Click in the toolbar on the `Submit` button. This will create the polygon without connecting the last point to the first. - 3. Change the tool. This will also create the polygon without connecting the last point to the first. -4. If you want to remove the selected point, click on the `Delete` button in the toolbar. -5. To edit the last point, click on the `Edit` button in the toolbar. This will allow you to click on a point and drag it to round it off. - -## Bézier Curves - -You can use this tool to create Bézier Curves which gives your shape a smooth curvature. -To adjust the curvature around a selected point, drag on the paper until a tangent line appears. - -## Toolbar - -The toolbar contains the following buttons: - -* `Edit`: Allows you to change the selected point of the polygon. -* `Delete`: Allows you to delete the selected point of the polygon. -* `Finish`: Connects the last point to the first point, completing the polygon. -* `Submit`: Completes the polygon without connecting the last point to the first. -* Stroke width: Adjusts the width of the polygon's stroke. -* Colors to change the stroke color of the polygon. - -## Configuration - -| Property | Default | Description | -| -----------: | :---------: | :----------------------------------------------- | -| Stroke width | 5 | The width of the stroke | -| Color | Black | The color that will be drawn | -| Fill | Transparent | The color that will be drawn inside the polygon. | diff --git a/docs/src/content/docs/docs/v2/tools/polygon.mdx b/docs/src/content/docs/docs/v2/tools/polygon.mdx new file mode 100644 index 000000000000..b2aa3e6bc845 --- /dev/null +++ b/docs/src/content/docs/docs/v2/tools/polygon.mdx @@ -0,0 +1,41 @@ +--- +title: Polygon tool +--- +import {Check, Trash, Polygon} from "@phosphor-icons/react"; + +With this tool you can create custom shapes. + +## Usage + +1. Click on the canvas to add the first point of the polygon. +2. Click again to add subsequent points. +3. Now you have three options to finish the polygon: + 1. Click in the toolbar on the `Finish` button. This will connect the last point to the first + point. + 2. Click in the toolbar on the `Submit` button. This will create the polygon without connecting + the last point to the first. + 3. Change the tool. This will also create the polygon without connecting the last point to the + first. +4. If you want to remove the selected point, click on the `Delete` button in the toolbar. +5. To edit a point, click on click it and drag it to round it off. To reenter edit mode, right-click on the selected polygon and click `Edit`. + +## Bézier Curves + +You can use this tool to create Bézier Curves which gives your shape a smooth curvature. +To adjust the curvature around a selected point, drag on the paper until a tangent line appears. + +## Toolbar + +While in Polygon Edit mode the toolbar contains the following buttons: + +* `Delete` - Allows you to delete the selected point of the polygon. +* `Finish` - Connects the last point to the first point, completing the polygon. +* `Submit` - Completes the polygon without connecting the last point to the first. + +## Configuration + +| Property | Default | Description | +|-------------:|:-------------------------:|:------------------------------------------------------------------------------| +| Stroke width | 5 | The width of the stroke | +| Color | Solid Color (Black) | [Paint options](../../paint_options/) for the color of the polygon | +| Fill | Solid Color (Transparent) | [Paint options](../../paint_options/) for the color drawn inside the polygon. | | diff --git a/docs/src/content/docs/docs/v2/tools/redo.md b/docs/src/content/docs/docs/v2/tools/redo.md index 8371adf3aba9..de44e24ca619 100644 --- a/docs/src/content/docs/docs/v2/tools/redo.md +++ b/docs/src/content/docs/docs/v2/tools/redo.md @@ -4,8 +4,8 @@ title: Redo tool :::note[⚡ Action tool] -This is a special tool. -You can't select it and it will run the action if you click on it. +This is a special action tool. +It can't be selected and will run the action immediately when you click on it. ::: diff --git a/docs/src/content/docs/docs/v2/tools/select.md b/docs/src/content/docs/docs/v2/tools/select.md index 4cf5258cfac6..2dc50c6956d4 100644 --- a/docs/src/content/docs/docs/v2/tools/select.md +++ b/docs/src/content/docs/docs/v2/tools/select.md @@ -7,6 +7,20 @@ With this tool you can select elements. ## Configuration -| Property | Default | Description | -| -------: | :-------: | :----------------------------------------------- | -| Mode | Rectangle | Mode of selection. Valid are Rectangle and Lasso | +| Property | Default | Description | +|---------:|:--------------:|:-------------------------------------------------| +| Mode | Rectangle | Mode of selection. Valid are Rectangle and Lasso | +| Hit mode | Touch anywhere | [Hit shapes mode](#hit-mode) | + +### Hit mode + +Defines which part of an element needs to be touched for it to be selected. + +Note that Touch edges and Touch anywhere are only relevant for [shapes](../shape) +and [polygons](../polygon). + +| Mode | Description | +|------------------------------:|:----------------------------------------------------------------------------| +| Full Selection | The selected area needs to fully enclose the element. | +| Select when touching edges | The shape will be selected when the selected area intersects any edge. | +| Select when touching anywhere | The shape will be selected when any part of it is inside the selected area. | diff --git a/docs/src/content/docs/docs/v2/tools/shape.md b/docs/src/content/docs/docs/v2/tools/shape.md index d244e056820a..a20f59cf7801 100644 --- a/docs/src/content/docs/docs/v2/tools/shape.md +++ b/docs/src/content/docs/docs/v2/tools/shape.md @@ -9,41 +9,18 @@ Use `ctrl` to have the same height and width and `shift` to draw from the center ## Configuration -| Property | Default | Description | -| -------------: | :-------: | :----------------------------------------------------------------------------------------------------------------------------------------------- | -| Color | Black | The stroke color that will be drawn | -| Stroke width | 5 | The width of the stroke | -| Stroke style | Solid | The style of the stroke. Other styles can use dash and gap lengths. | -| Dash length | 1 | The length of the dash when using a non-solid stroke style | -| Gap length | 1 | The length of the gap when using a non-solid stroke style | -| Zoom dependent | false | This will change the stroke width based on the zoom level. | -| Shape | Rectangle | The shape that will be drawn | -| Width | 0 | The fixed width of the area. If set to 0, the width will be calculated automatically. | -| Height | 0 | The fixed height of the area. If set to 0, the height will be calculated automatically. | -| Aspect ratio | 0 | The fixed aspect ratio of the area. If set to 0, the aspect ratio will be calculated automatically. An aspect ratio is defined as width / height. | -| Center | false | Draws the shape from the center instead of from the corner. | - -### Shape types - -#### Rectangle - -| Property | Default | Description | -| ------------: | :---------: | :------------------------------------------------ | -| Fill | Transparent | The color that will be drawn inside the rectangle | -| Corner radius | 0, 0, 0, 0 | The radius of the corners of the rectangle | - -#### Triangle - -| Property | Default | Description | -| ---------: | :---------: | :----------------------------------------------- | -| Fill | Transparent | The color that will be drawn inside the triangle | - -#### Circle - -| Property | Default | Description | -| ---------: | :---------: | :--------------------------------------------- | -| Fill | Transparent | The color that will be drawn inside the circle | - -#### Line - -*No configuration available.* +| Property | Default | Description | +|---------------:|:-------------------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------| +| Center | false | Draws the shape from the center instead of from the corner. | +| Width | 0 | The fixed width of the area. If set to 0, the width will be calculated automatically. | +| Height | 0 | The fixed height of the area. If set to 0, the height will be calculated automatically. | +| Aspect ratio | 0 | The fixed aspect ratio of the area. If set to 0, the aspect ratio will be calculated automatically. An aspect ratio is defined as width / height. | +| Stroke width | 5 | The width of the stroke | +| Stroke style | Solid | The style of the stroke. Other styles can use dash and gap lengths. | +| Dash length | 1 | The length of the dash when using a non-solid stroke style | +| Gap length | 1 | The length of the gap when using a non-solid stroke style | +| Color | Solid Color (Black) | [Paint options](../../paint_options/) for the color of the shape | +| Shape | Rectangle | The shape that will be drawn. Available shapes are Rectangle, Triangle, Circle and Line | +| Corner radius | 0, 0, 0, 0 | The radius of the corners when shape is Rectangle | +| Fill | Solid Color (Transparent) | [Paint options](../../paint_options/) for the color drawn inside the shape. Not available for Line shapes | | +| Zoom dependent | false | This will change the stroke width based on the zoom level. | \ No newline at end of file diff --git a/docs/src/content/docs/docs/v2/tools/texture.md b/docs/src/content/docs/docs/v2/tools/texture.md index 031dc0c05b3c..1e9f075f64e2 100644 --- a/docs/src/content/docs/docs/v2/tools/texture.md +++ b/docs/src/content/docs/docs/v2/tools/texture.md @@ -3,7 +3,8 @@ title: Texture --- The texture tool allows you to add a background texture to a small area of the canvas. -Use it when only a part of the canvas should have a background pattern. To change the background of the whole page, use the [background settings](../../background) instead. +Use it when only a part of the canvas should have a background pattern. To change the background of +the whole page, use the [background settings](../../background) instead. ## Usage @@ -11,18 +12,12 @@ Use it when only a part of the canvas should have a background pattern. To chang 2. Choose the texture and its constraints in the tool configuration. 3. Drag on the canvas to create the textured area. -The created texture behaves like a surface element. You can move, resize, arrange, or delete it like other elements. +The created texture behaves like a surface element. You can move, resize, arrange, or delete it like +other elements. ## Configuration -| Property | Default | Description | -| -------: | :-----: | :---------- | -| Zoom dependent | false | This will change the stroke width based on the zoom level. | -| Texture | Pattern | The pattern that should be added. | -| Width | 0 | The fixed width of the area. If set to `0`, the width will be calculated automatically. | -| Height | 0 | The fixed height of the area. If set to `0`, the height will be calculated automatically. | -| Aspect ratio | 0 | The fixed aspect ratio of the area. If set to `0`, the aspect ratio will be calculated automatically. An aspect ratio is defined as width / height. | - -Valid types for texture are: - -* [Pattern](../../background#pattern) +| Property | Description | +|----------------:|:---------------------------------------------------------------------------------------------------------| +| Texture | The pattern preset that should be used. | +| Pattern options | [Configuration](../../background#pattern-layers) for the background color, horizontal and vertical lines | diff --git a/docs/src/content/docs/docs/v2/tools/undo.md b/docs/src/content/docs/docs/v2/tools/undo.md index a7d87d8d2c23..18fdbd192064 100644 --- a/docs/src/content/docs/docs/v2/tools/undo.md +++ b/docs/src/content/docs/docs/v2/tools/undo.md @@ -4,8 +4,8 @@ title: Undo tool :::note[⚡ Action tool] -This is a special tool. -You can't select it and it will run the action if you click on it. +This is a special action tool. +It can't be selected and will run the action immediately when you click on it. ::: diff --git a/docs/src/translations/en.json b/docs/src/translations/en.json index d1a951ae6730..099bfd2b1374 100644 --- a/docs/src/translations/en.json +++ b/docs/src/translations/en.json @@ -1,14 +1,16 @@ { "guides": "Guides", "introduction": "Introduction", + "context_menu": "Context menu", "areas": "Areas", "background": "Background", "color_picker": "Color picker", + "paint_options": "Paint options", "layers": "Layers", "migrating": "Migrating", "pack": "Pack", "pages": "Pages", - "templates" : "Templates", + "templates": "Templates", "shortcuts": "Shortcuts", "waypoints": "Waypoints", "add": "Add", @@ -19,7 +21,6 @@ "pen": "Pen", "select": "Select", "hand": "Hand", - "path_eraser": "Path eraser", "eraser": "Eraser", "undo": "Undo", "redo": "Redo", From 427b53271f0425dd3ef6943d0a03a65a658b6459 Mon Sep 17 00:00:00 2001 From: Nezznee Date: Mon, 27 Jul 2026 23:14:16 +0200 Subject: [PATCH 117/117] Improve documentation --- docs/astro.config.mjs | 8 +- docs/src/content/docs/docs/v2/color_picker.md | 36 ---- .../docs/v2/{paint_options.md => colors.md} | 47 ++++- .../src/content/docs/docs/v2/context_menu.mdx | 10 +- docs/src/content/docs/docs/v2/shortcuts.md | 164 +++++++++++++++--- .../content/docs/docs/v2/tools/full_screen.md | 3 +- .../content/docs/docs/v2/tools/polygon.mdx | 4 +- docs/src/content/docs/docs/v2/tools/select.md | 2 +- docs/src/translations/en.json | 3 +- 9 files changed, 198 insertions(+), 79 deletions(-) delete mode 100644 docs/src/content/docs/docs/v2/color_picker.md rename docs/src/content/docs/docs/v2/{paint_options.md => colors.md} (59%) diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 3d318ad8d1dd..a461a7902dd9 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -82,12 +82,8 @@ export default defineConfig({ link: "/docs/v2/background/", }, { - ...getSidebarTranslatedLabel("Color picker"), - link: "/docs/v2/color_picker/", - }, - { - ...getSidebarTranslatedLabel("Paint Options"), - link: "/docs/v2/paint_options/", + ...getSidebarTranslatedLabel("Colors"), + link: "/docs/v2/colors/", }, { ...getSidebarTranslatedLabel("Layers"), diff --git a/docs/src/content/docs/docs/v2/color_picker.md b/docs/src/content/docs/docs/v2/color_picker.md deleted file mode 100644 index cfb317b313b2..000000000000 --- a/docs/src/content/docs/docs/v2/color_picker.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Color picker ---- - -Colors can be selected by using two methods: The color toolbar and the color picker overlay. - -To update the color palette, read the [pack documentation](/docs/v2/pack). - -## Color toolbar - -![Color toolbar](color_toolbar.png) - -If this is enabled in the settings, a color toolbar will be shown when a colorable tool is selected. This toolbar allows you to quickly select a color from a predefined set of colors. Click on the plus icon to select a custom color. - -## Color picker overlay - -![Color picker overlay](color_picker_overlay.png) - -This overlay can be opened by clicking on a property tile that is colorable, for example inside the properties panel of the pen tool. Click on a color to select it. Click on the custom button to open the custom color picker. - -If you want to delete a color from the palette, right click on it (or long press on touch devices) and select delete. - -### Custom color picker - -![Custom color picker](color_picker.png) - -Here you can select any color you want. On the left you can see a color wheel. Under it you can select the brightness of the color. -Note: if you choose a darker color on the bottom, the wheel selection gets less precise. - -Under the brightness slider you can see a preview of the selected color. You can also enter a hex code to select a color. It is specified as `#RRGGBB`, where `RR` is the red value, `GG` is the green value, and `BB` is the blue value in hexadecimal notation. - -On the right you can see the red, green and blue values that make up the color. These values can be changed by dragging the sliders or by entering a value between 0 and 255. Pin the color to add it to the color palette. - -You can use the buttons above to toggle between RGB, HSV, and HSL views. - -Clicking the Eye dropper button adds the [Eye dropper tool](../tools/eye_dropper) as a [temporary tool](../tools#temporary-tools). \ No newline at end of file diff --git a/docs/src/content/docs/docs/v2/paint_options.md b/docs/src/content/docs/docs/v2/colors.md similarity index 59% rename from docs/src/content/docs/docs/v2/paint_options.md rename to docs/src/content/docs/docs/v2/colors.md index 92106e84a08b..1194d85ce5ce 100644 --- a/docs/src/content/docs/docs/v2/paint_options.md +++ b/docs/src/content/docs/docs/v2/colors.md @@ -1,10 +1,47 @@ --- -title: Paint Options +title: Color picker --- +Colors can be selected by using two methods: The color toolbar and the color picker overlay. + +To update the color palette, read the [pack documentation](/docs/v2/pack). + For some [surface tools](../add/#surfaces) like the [Pen](../tools/pen/), [Shape](../tools/shape/) -and [Polygon](../tools/polygon) tools, you can configure how the paint of the tool behaves. This -section will cover the different Paint modes for both the **color** and the **fill** properties. +and [Polygon](../tools/polygon) tools, you can further customize the coloring of the tool. This +section will cover the different modes for both the **color** and the **fill** properties with gradients or images. See [Further customization](#further-customization). + +## Color toolbar + +![Color toolbar](color_toolbar.png) + +If this is enabled in the settings, a color toolbar will be shown when a colorable tool is selected. This toolbar allows you to quickly select a color from a predefined set of colors. Click on the plus icon to select a custom color. + +## Color picker overlay + +![Color picker overlay](color_picker_overlay.png) + +This overlay can be opened by clicking on a property tile that is colorable, for example inside the properties panel of the pen tool. Click on a color to select it. Click on the custom button to open the custom color picker. + +If you want to delete a color from the palette, right click on it (or long press on touch devices) and select delete. + +### Custom color picker + +![Custom color picker](color_picker.png) + +Here you can select any color you want. On the left you can see a color wheel. Under it you can select the brightness of the color. +Note: if you choose a darker color on the bottom, the wheel selection gets less precise. + +Under the brightness slider you can see a preview of the selected color. You can also enter a hex code to select a color. It is specified as `#RRGGBB`, where `RR` is the red value, `GG` is the green value, and `BB` is the blue value in hexadecimal notation. + +On the right you can see the red, green and blue values that make up the color. These values can be changed by dragging the sliders or by entering a value between 0 and 255. Pin the color to add it to the color palette. + +You can use the buttons above to toggle between RGB, HSV, and HSL views. + +Clicking the Eye dropper button adds the [Eye dropper tool](../tools/eye_dropper) as a [temporary tool](../tools#temporary-tools). + +--- + +## Further customization ## Solid Color @@ -44,8 +81,6 @@ Make your strokes transition between colors |--------------:|:--------------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------| | Center | (0.5,0.5) | Geometric middle of the boundary circle at 100% offset; percentually relative to the top left corner | | Center radius | 0.5 | Radius of the boundary circle; percentually relative to the top left corner | -| Focal Point | off (same as center) | Where the 0% offset position is placed. If it's not same as center then a ellipse instead of a circle will form. | +| Focal Point | off (same as center) | Where the 0% offset position is placed. If it's not same as center then an ellipse instead of a circle will form. | | Focal radius | 0 | Radius of the inner circle where the offset is at 0%. Behind it the offset increases. | | Color stops | 2 Color stops | The offset of a Color stop percentually defines where between focal point and the boundary the color is placed. There are color and alpha options | - - diff --git a/docs/src/content/docs/docs/v2/context_menu.mdx b/docs/src/content/docs/docs/v2/context_menu.mdx index b95a48c7e20f..c96af839423f 100644 --- a/docs/src/content/docs/docs/v2/context_menu.mdx +++ b/docs/src/content/docs/docs/v2/context_menu.mdx @@ -12,15 +12,16 @@ import {Scissors, Stack, Export, PlusCircle, + Clipboard, Polygon, ArrowUpRight, } from "@phosphor-icons/react"; -To access the Context menu right click on a selection. +To access the Context menu [long press](../shortcuts#document-changes) on a selection. ## Actions -The Context menu contains actions that can be executed on the selected elements. Selections with only one element may have additional action specific to that element. +The Context menu contains actions that can be executed on the selected elements. Selections with only one element may have special actions specific to that element. * `Cut` - Deletes the selection and adds it to the clipboard. * `Copy` - Adds the selection it to the clipboard. @@ -32,9 +33,10 @@ The Context menu contains actions that can be executed on the selected elements. * `Change collection` - Changes the [Collection](../tools/collection) this selection belongs to. Here you need to enter the name of the (maybe nonexistent) collection. * `Move to layer` - Changes the [Layer](../layers) this selection belongs to. * `Export` - Exports the selection as an Image, SVG or PDF. -* `Add to pack` - Adds the selection as an asset to a [Pack](../pack/#adding-a-component-into-a-pack) ! +* `Add to pack` - Adds the selection as an asset to a [Pack](../pack/#adding-a-component-into-a-pack) -Element specific actions: +Special actions: +* `Paste` - Using long press with the [Select tool](../tools/select) on an empty canvas position pastes the clipboard at that point. * `Edit` - Enters edit mode for this specific [Polygon](../tools/polygon) ### Arrange options diff --git a/docs/src/content/docs/docs/v2/shortcuts.md b/docs/src/content/docs/docs/v2/shortcuts.md index 5518d97b9cf8..a898acf7a308 100644 --- a/docs/src/content/docs/docs/v2/shortcuts.md +++ b/docs/src/content/docs/docs/v2/shortcuts.md @@ -2,11 +2,117 @@ title: Shortcuts --- +Shortcuts are a way to map specific inputs to an action that influences the editor. + +To begin, go to `Settings` → `Inputs` and then select the input method you want to configure, such +as `Mouse`, `Touch`, `Keyboard` or `Pen`. You will be presented with a list of configurable inputs +and the actions they are currently mapped to. + +These actions are divided into [tool activators](#tool-activators) +and [document actions](#document-actions). + +## Tool activators + +You can customize your controls by changing which tools your inputs map to. + +**Note:** Tool activators will be ignored while certain tools are selected, such as the Select tool, +the Label tool, and the Area tool. + +* `Active Tool`: The input will act as the currently selected tool on the toolbar. +* `Hand Tool`: The input will use the hand tool as a [temporary tool](tools/#temporary-tools), + allowing you to move around the canvas. +* `Specific Tool on Toolbar`: The input will use the specified tool on the toolbar as + a [temporary tool](../#temporary-tools), based on the position you specify. Positions are counted + starting from the left, so if you specify position `1`, the first tool on the left will be + selected. See the screenshot below for an example of how position numbers are counted. For + information about how to reorder your tools, + see [Customizing the Toolbar](../intro/#customizing-the-toolbar). + +![toolbar numbered](toolbar_numbered.png) + +## Document actions + +* `None`: Nothing happens +* `Long press`: Opens the [Context menu](../context-menu) +* `Search`: Searches the document for pages and tools +* `Undo`: Triggers the [Undo tool](../tools/undo) +* `Redo`: Triggers the [Redo tool](../tools/redo) +* `Background`: Opens the [Background dialog](../background) +* `Save`: Saves the document state +* `Change path`: Changes where the document is stored relative to the `Documents` folder + in [Data directory](../storage/#data-directory). +* `Zoom in`: Zooms into the canvas at the current position. See [Camera](../utilities/camera). +* `Zoom out`: Zooms out of the canvas at the current position. See [Camera](../utilities/camera). +* `Full screen`: Toggles [Full screen](../tools/full_sreen) +* `Hide UI`: Hides everything except the canvas. To leave this view, click the `Exit` button on the + bottom right. +* `Next page`: Navigates to the next [page](../pages) +* `Previous page`: Navigates to the previous [page](../pages) +* `Select all`: Selects all elements on the canvas +* `Paste`: Pastes the clipboard +* `Tool 1-10`: Switches the active tool to the specified toolbar position + +--- + +## Mouse + +### Mouse configurations + +| Property | Default | Description | +|--------------------------:|:-------:|:--------------------------------------------------------------------------| +| Hide cursor while drawing | true | Hides the mouse pointer while you draw, so it does not cover your stroke. | + +### Mouse shortcuts + +**Tool activators**: + +* `Left`: When holding the left mouse button. Defaults to `Active Tool` +* `Middle`: When holding the mouse wheel. Defaults to `Hand Tool` +* `Rigth`: When holding the right mouse button. Defaults to `Toolbar Position 2` + +**Document actions**: + +*By default, the touch document actions are all set to `None`.* + +* `Double Left`: A double click on the left mouse button +* `Triple Left`: A triple click on the left mouse button +* `Double Middle`: A double click on the mouse wheel +* `Triple Middle`: A triple click on the mouse wheel +* `Double Right`: A double click on the right mouse button +* `Triple Right`: A triple click on the right mouse button + +## Touch + +### Touch configurations + +| Property | Default | Description | +|----------------:|:-------:|:--------------------------------------------------------------------------------------------| +| Input gestures | true | Lets you move and zoom the canvas with touch gestures, even while drawing tool is selected. | +| Move on gesture | true | Lets multi-touch gestures move the canvas instead of interacting with note content. | + +### Touch shortcuts + +**Tool activators**: + +* `Touch`: When touching the screen. Defaults to `Active Tool` + +**Document actions**: + +*By default, the touch document actions are all set to `None`.* + +* `Double press action`: A double-tap +* `Triple press action`: A triple-tap ## Keyboard -There are a few shortcuts that you can use in the editor. -Some of them are written below the buttons. +Keyboard actions are divided into the categories hold shortcuts +for **tool activators**, general and project +for **document actions**. + +### Hold shortcuts + +*There is no default configuration. You may add any key mappings +to **tool activators**.* ### General @@ -15,44 +121,60 @@ Some of them are written below the buttons. * `Ctrl` + `E`: Export file * `Ctrl` + `Shift` + `E`: Export file (text based) * `Ctrl` + `Alt` + `Shift` + `E`: Export file as image -* `Ctrl` + `Alt` + `E`: Export file as svg -* `Ctrl` + `Shift` + `P`: Export file as pdf +* `Ctrl` + `Shift` + `P`: Export file as PDF +* `Ctrl` + `Alt` + `E`: Export file as SVG * `Ctrl` + `Alt` + `P`: Open packs * `Ctrl` + `Alt` + `S`: Open settings +* `Escape`: Escape ### Project * `Ctrl` + `K`: Open search * `Ctrl` + `Z`: Undo * `Ctrl` + `Y`: Redo -* `Ctrl` + `Shift` + `P`: Open waypoints dialog * `Ctrl` + `B`: Open background dialog * `Ctrl` + `S`: Save * `Alt` + `S`: Change path -* `Ctrl` + (`1` - `0`): Switch to tool * `Ctrl` + `+`: Zoom in * `Ctrl` + `-`: Zoom out +* `F11`: Full screen +* `F12`: Hide UI +* `Arrow Right`: Next slide in presentation +* `Arrow Left`: Previous slide in presentation +* `Page Down`: Next page +* `Page Up`: Previous page +* `Ctrl`: Pause presentation +* `Ctrl` + `A`: Select all +* `Ctrl` + `V`: Pastes the clipboard +* `Ctrl` + (`1` - `0`): Switch to tool ## Pen -By default, the pen is configured to function as the following: +### Pen configurations -* `Pen`: configured as pen. -* `First` (Primary button, if supported): Change to hand tool while pressed. -* `Second` (Secondary button, if supported): Change to second tool (see [configure](#configure) section below) while pressed. +| Property | Values | Description | +|---------------------:|:--------------------------------:|:-----------------------------------------------------------------------------------------------------------------| +| Pen only input | Automatic, Always on, Always off | Prevents accidental marks from your hand or mouse when only pen input can draw. | +| Show pen only toggle | true, false | Shows a quick pen-only switch in the editor after Butterfly detects a pen. | +| Ignore pressure | Never, First, Always | Controls whether a pen pressure changes the stroke and works around inaccurate pressure readings from some pens. | -## Configuring +### Pen shortcuts -You can customize your controls by changing which tools your inputs map to. - -**Note:** Input configurations will be ignored while certain tools are selected, such as the Lasso Select tool, the Rectangle Select tool, the Label tool, and the Area tool. +By default, the pen is configured to function with the +following **tool activators**: -To begin, go to `Settings` → `Inputs` and then select the input method you want to configure, such as `Mouse`, `Touch`, or `Pen`. You will be presented with a list of configurable inputs and the tools they are currently mapped to. +* `Pen`: Using the pen normally. Defaults to `Active Tool` +* `Inverted Pen`: Using the pen in inverted mode. Defaults to `Toobar Position 4` +* `First`: While holding its primary button, if supported. Defaults to `Toobar Position 3` (often path-eraser) +* `Second`: While holding its secondary button, if supported. Defaults to `Toolbar Position 2` -After selecting an input, you will have 3 options: +*By default, the pen **document actions** are all set to `None`.* -- `Active Tool`: The input will act as the currently selected tool on the toolbar. -- `Hand Tool`: The input will temporarily switch to the hand tool, allowing you to move around the canvas. -- `Specific Tool on Toolbar`: The input will temporarily switch to a tool on your toolbar, based on the position number you specify. Positions are counted starting from the left, so if you specify position `1`, the first tool on the left will be selected. See the screenshot below for an example of how position numbers are counted. For information about how to reorder your tools, see [Customizing the Toolbar](../intro/#customizing-the-toolbar). - -![toolbar numbered](toolbar_numbered.png) +* `Double Pen`: Double-tapping with a pen +* `Triple Pen`: Triple-tapping with a pen +* `Double Inverted Pen`: Double-tapping with a pen in inverted mode +* `Triple Inverted Pen`: Triple-tapping with using a pen in inverted mode +* `Double First`: Double-tapping with a pen while holding its primary button +* `Triple First`: Triple-tapping with a pen while holding its primary button +* `Double Second`: Double-tapping with a pen while holding its secondary button +* `Triple Second`: Triple-tapping with a pen while holding its primary button \ No newline at end of file diff --git a/docs/src/content/docs/docs/v2/tools/full_screen.md b/docs/src/content/docs/docs/v2/tools/full_screen.md index 6e9d12d34f61..9245eb2680da 100644 --- a/docs/src/content/docs/docs/v2/tools/full_screen.md +++ b/docs/src/content/docs/docs/v2/tools/full_screen.md @@ -9,4 +9,5 @@ It can't be selected and will run the action immediately when you click on it. ::: -When clicking on this tool you can toggle full screen. +Clicking this tool maximizes the window if on desktop, hides the sidebar and minimizes the toolbar to make place for the canvas. +A second click will restore the previous state. diff --git a/docs/src/content/docs/docs/v2/tools/polygon.mdx b/docs/src/content/docs/docs/v2/tools/polygon.mdx index b2aa3e6bc845..d7a2b9c5a192 100644 --- a/docs/src/content/docs/docs/v2/tools/polygon.mdx +++ b/docs/src/content/docs/docs/v2/tools/polygon.mdx @@ -17,7 +17,7 @@ With this tool you can create custom shapes. 3. Change the tool. This will also create the polygon without connecting the last point to the first. 4. If you want to remove the selected point, click on the `Delete` button in the toolbar. -5. To edit a point, click on click it and drag it to round it off. To reenter edit mode, right-click on the selected polygon and click `Edit`. +5. To edit a point, click on click it and drag it to round it off. To reenter edit mode, open it's [Context menu](../../context_menu) and click `Edit`. ## Bézier Curves @@ -26,7 +26,7 @@ To adjust the curvature around a selected point, drag on the paper until a tange ## Toolbar -While in Polygon Edit mode the toolbar contains the following buttons: +While in Polygon edit mode the toolbar contains the following buttons: * `Delete` - Allows you to delete the selected point of the polygon. * `Finish` - Connects the last point to the first point, completing the polygon. diff --git a/docs/src/content/docs/docs/v2/tools/select.md b/docs/src/content/docs/docs/v2/tools/select.md index 2dc50c6956d4..b486ec4fc635 100644 --- a/docs/src/content/docs/docs/v2/tools/select.md +++ b/docs/src/content/docs/docs/v2/tools/select.md @@ -3,7 +3,7 @@ title: Select tool --- -With this tool you can select elements. +With this tool you can select elements and access their [Context menu](../../context_menu). ## Configuration diff --git a/docs/src/translations/en.json b/docs/src/translations/en.json index 099bfd2b1374..17f36d54f68c 100644 --- a/docs/src/translations/en.json +++ b/docs/src/translations/en.json @@ -4,8 +4,7 @@ "context_menu": "Context menu", "areas": "Areas", "background": "Background", - "color_picker": "Color picker", - "paint_options": "Paint options", + "colors": "Colors", "layers": "Layers", "migrating": "Migrating", "pack": "Pack",