diff --git a/api/lib/src/models/archive.dart b/api/lib/src/models/archive.dart index bf64bc8521ed..941d5ff6db8e 100644 --- a/api/lib/src/models/archive.dart +++ b/api/lib/src/models/archive.dart @@ -1,6 +1,7 @@ // 16:9 const kThumbnailWidth = 640; const kThumbnailHeight = 360; +const kThumbnailRatio = 16 / 9; // Archive specific const kMetaInfoArchiveDirectory = 'META-INF'; diff --git a/api/lib/src/models/tool.dart b/api/lib/src/models/tool.dart index e22d29405a5e..71e6e1905462 100644 --- a/api/lib/src/models/tool.dart +++ b/api/lib/src/models/tool.dart @@ -77,7 +77,7 @@ sealed class Tool with _$Tool { @Default('') String name, @Default('') String displayIcon, @Default(LabelMode.text) LabelMode mode, - @Default(true) bool zoomDependent, + @Default(false) bool zoomDependent, @Default(BasicColors.black) int foreground, @Default(PackAssetLocation()) PackAssetLocation styleSheet, @Default(2.0) double scale, @@ -86,7 +86,7 @@ sealed class Tool with _$Tool { factory Tool.pen({ @Default('') String name, @Default('') String displayIcon, - @Default(true) bool zoomDependent, + @Default(false) bool zoomDependent, @Default(0.5) double shapeDetectionTime, @Default(false) bool shapeDetectionEnabled, @Default(PenProperty()) PenProperty property, @@ -135,7 +135,7 @@ sealed class Tool with _$Tool { factory Tool.shape({ @Default('') String name, @Default('') String displayIcon, - @Default(true) bool zoomDependent, + @Default(false) bool zoomDependent, @Default(0) double constrainedWidth, @Default(0) double constrainedHeight, @Default(0) double constrainedAspectRatio, @@ -181,7 +181,7 @@ sealed class Tool with _$Tool { factory Tool.texture({ @Default('') String name, @Default('') String displayIcon, - @Default(true) bool zoomDependent, + @Default(false) bool zoomDependent, @Default(0) double constrainedWidth, @Default(0) double constrainedHeight, @Default(0) double constrainedAspectRatio, diff --git a/api/lib/src/models/tool.freezed.dart b/api/lib/src/models/tool.freezed.dart index 31ac41864475..0fb08e085edb 100644 --- a/api/lib/src/models/tool.freezed.dart +++ b/api/lib/src/models/tool.freezed.dart @@ -769,7 +769,7 @@ class _$LabelToolImpl extends LabelTool { {this.name = '', this.displayIcon = '', this.mode = LabelMode.text, - this.zoomDependent = true, + this.zoomDependent = false, this.foreground = BasicColors.black, this.styleSheet = const PackAssetLocation(), this.scale = 2.0, @@ -929,7 +929,7 @@ class _$PenToolImpl extends PenTool { _$PenToolImpl( {this.name = '', this.displayIcon = '', - this.zoomDependent = true, + this.zoomDependent = false, this.shapeDetectionTime = 0.5, this.shapeDetectionEnabled = false, this.property = const PenProperty(), @@ -1805,7 +1805,7 @@ class _$ShapeToolImpl extends ShapeTool { _$ShapeToolImpl( {this.name = '', this.displayIcon = '', - this.zoomDependent = true, + this.zoomDependent = false, this.constrainedWidth = 0, this.constrainedHeight = 0, this.constrainedAspectRatio = 0, @@ -2691,7 +2691,7 @@ class _$TextureToolImpl extends TextureTool { _$TextureToolImpl( {this.name = '', this.displayIcon = '', - this.zoomDependent = true, + this.zoomDependent = false, this.constrainedWidth = 0, this.constrainedHeight = 0, this.constrainedAspectRatio = 0, diff --git a/api/lib/src/models/tool.g.dart b/api/lib/src/models/tool.g.dart index 2732bb24af43..95a0b5b33cb1 100644 --- a/api/lib/src/models/tool.g.dart +++ b/api/lib/src/models/tool.g.dart @@ -92,7 +92,7 @@ _$LabelToolImpl _$$LabelToolImplFromJson(Map json) => _$LabelToolImpl( displayIcon: json['displayIcon'] as String? ?? '', mode: $enumDecodeNullable(_$LabelModeEnumMap, json['mode']) ?? LabelMode.text, - zoomDependent: json['zoomDependent'] as bool? ?? true, + zoomDependent: json['zoomDependent'] as bool? ?? false, foreground: (json['foreground'] as num?)?.toInt() ?? BasicColors.black, styleSheet: json['styleSheet'] == null ? const PackAssetLocation() @@ -122,7 +122,7 @@ const _$LabelModeEnumMap = { _$PenToolImpl _$$PenToolImplFromJson(Map json) => _$PenToolImpl( name: json['name'] as String? ?? '', displayIcon: json['displayIcon'] as String? ?? '', - zoomDependent: json['zoomDependent'] as bool? ?? true, + zoomDependent: json['zoomDependent'] as bool? ?? false, shapeDetectionTime: (json['shapeDetectionTime'] as num?)?.toDouble() ?? 0.5, shapeDetectionEnabled: json['shapeDetectionEnabled'] as bool? ?? false, @@ -254,7 +254,7 @@ const _$LaserAnimationEnumMap = { _$ShapeToolImpl _$$ShapeToolImplFromJson(Map json) => _$ShapeToolImpl( name: json['name'] as String? ?? '', displayIcon: json['displayIcon'] as String? ?? '', - zoomDependent: json['zoomDependent'] as bool? ?? true, + zoomDependent: json['zoomDependent'] as bool? ?? false, constrainedWidth: (json['constrainedWidth'] as num?)?.toDouble() ?? 0, constrainedHeight: (json['constrainedHeight'] as num?)?.toDouble() ?? 0, constrainedAspectRatio: @@ -395,7 +395,7 @@ Map _$$ExportToolImplToJson(_$ExportToolImpl instance) => _$TextureToolImpl _$$TextureToolImplFromJson(Map json) => _$TextureToolImpl( name: json['name'] as String? ?? '', displayIcon: json['displayIcon'] as String? ?? '', - zoomDependent: json['zoomDependent'] as bool? ?? true, + zoomDependent: json['zoomDependent'] as bool? ?? false, constrainedWidth: (json['constrainedWidth'] as num?)?.toDouble() ?? 0, constrainedHeight: (json['constrainedHeight'] as num?)?.toDouble() ?? 0, constrainedAspectRatio: diff --git a/app/lib/dialogs/template.dart b/app/lib/dialogs/template.dart index 1870eecdfaa5..120dfb4c54e2 100644 --- a/app/lib/dialogs/template.dart +++ b/app/lib/dialogs/template.dart @@ -3,9 +3,11 @@ import 'package:butterfly/actions/new.dart'; import 'package:butterfly/api/file_system.dart'; import 'package:butterfly/api/save.dart'; import 'package:butterfly/cubits/settings.dart'; +import 'package:butterfly/dialogs/name.dart'; +import 'package:butterfly/visualizer/tool.dart'; import 'package:butterfly/widgets/connection_button.dart'; +import 'package:butterfly/widgets/option_button.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:flutter_gen/gen_l10n/app_localizations.dart'; @@ -16,6 +18,19 @@ import '../bloc/document_bloc.dart'; import '../widgets/editable_list_tile.dart'; import 'delete.dart'; +Future _overrideTools(TemplateFileSystem templateSystem, + DocumentBloc bloc, List templates) async { + final state = bloc.state; + if (state is! DocumentLoaded) return; + final tools = state.info.tools; + for (var template in templates) { + final info = template.getInfo(); + if (info == null) continue; + template = template.setInfo(info.copyWith(tools: tools)); + await templateSystem.updateFile(template.name ?? '', template); + } +} + class TemplateDialog extends StatefulWidget { final DocumentBloc? bloc; const TemplateDialog({super.key, this.bloc}); @@ -137,7 +152,7 @@ class _TemplateDialogState extends State { IconButton( onPressed: () => _showCreateDialog(widget.bloc!), tooltip: AppLocalizations.of(context).create, - icon: const PhosphorIcon(PhosphorIconsLight.plus), + icon: const PhosphorIcon(PhosphorIconsLight.floppyDisk), ) ], ], @@ -171,7 +186,7 @@ class _TemplateDialogState extends State { return _TemplateItem( template: template, fileSystem: _templateSystem, - replace: widget.bloc != null, + bloc: widget.bloc, selected: _selectedTemplates.contains(template.name), onSelected: () { setState(() { @@ -232,25 +247,15 @@ class _TemplateDialogState extends State { IconButton( icon: const PhosphorIcon(PhosphorIconsLight.wrench), tooltip: AppLocalizations.of(context).overrideTools, - onPressed: () async { - final state = widget.bloc!.state; - if (state is! DocumentLoaded) return; - final tools = state.info.tools; - for (final name in _selectedTemplates) { - var template = templates.firstWhereOrNull( - (element) => element.name == name); - if (template == null) continue; - final info = template.getInfo(); - if (info == null) continue; - template = template - .setInfo(info.copyWith(tools: tools)); - await _templateSystem.updateFile( - name, template); - } - setState(() { - _selectedTemplates.clear(); - }); - }, + onPressed: () => _overrideTools( + _templateSystem, + widget.bloc!, + templates + .where((element) => _selectedTemplates + .contains(element.name)) + .toList()) + .then((value) => + setState(() => _selectedTemplates.clear())), ), IconButton( icon: const PhosphorIcon(PhosphorIconsLight.trash), @@ -344,15 +349,16 @@ class _TemplateDialogState extends State { class _TemplateItem extends StatelessWidget { final NoteData template; final TemplateFileSystem fileSystem; + final DocumentBloc? bloc; final VoidCallback onChanged, onSelected, onUnselected; - final bool replace, selected; + final bool selected; const _TemplateItem({ required this.template, required this.fileSystem, required this.onChanged, - required this.replace, required this.selected, + this.bloc, required this.onSelected, required this.onUnselected, }); @@ -363,14 +369,28 @@ class _TemplateItem extends StatelessWidget { final settings = settingsCubit.state; final isDefault = settings.defaultTemplate == template.name; final metadata = template.getMetadata(); + final info = template.getInfo(); if (metadata == null) { return const SizedBox(); } final thumbnail = template.getThumbnail(); - const leading = PhosphorIcon( + const fallback = PhosphorIcon( PhosphorIconsLight.file, size: 48, ); + final leading = thumbnail != null + ? AspectRatio( + aspectRatio: kThumbnailRatio, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.memory( + thumbnail, + fit: BoxFit.cover, + cacheHeight: kThumbnailWidth, + cacheWidth: kThumbnailHeight, + )), + ) + : fallback; return EditableListTile( initialValue: metadata.name, subtitle: Text(metadata.description), @@ -378,6 +398,7 @@ class _TemplateItem extends StatelessWidget { height: 64, width: 96, child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Checkbox( value: selected, @@ -390,17 +411,7 @@ class _TemplateItem extends StatelessWidget { }, ), const SizedBox(width: 8), - Expanded( - child: thumbnail != null - ? Image.memory( - thumbnail, - fit: BoxFit.contain, - cacheWidth: 64, - cacheHeight: 64, - errorBuilder: (context, error, stackTrace) => leading, - ) - : leading, - ), + Flexible(child: leading), ], ), ), @@ -421,6 +432,79 @@ class _TemplateItem extends StatelessWidget { onChanged(); }, ), + if (bloc != null) + MenuItemButton( + leadingIcon: const PhosphorIcon(PhosphorIconsLight.trash), + child: Text(AppLocalizations.of(context).delete), + onPressed: () async { + _overrideTools(fileSystem, bloc!, [template]); + }, + ), + MenuItemButton( + leadingIcon: const PhosphorIcon(PhosphorIconsLight.copy), + child: Text(AppLocalizations.of(context).duplicate), + onPressed: () async { + final result = await showDialog( + context: context, builder: (ctx) => NameDialog( + value: template.name, + )); + if (result == null) return; + if (context.mounted) { + await fileSystem.createFileWithName(template.setMetadata(metadata.copyWith(name: result)), name: result); + onChanged(); + } + }, + ), + MenuItemButton( + leadingIcon: const PhosphorIcon(PhosphorIconsLight.info), + child: Text(AppLocalizations.of(context).information), + onPressed: () async { + showLeapBottomSheet( + context: context, + titleBuilder: (context) => Text(metadata.name), + leadingBuilder: (context) => Center(child: leading), + leadingWidth: 128, + childrenBuilder: (context) => [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Card.filled( + child: Padding( + padding: EdgeInsets.all(2), + child: Column(children: [ + if (metadata.description.isNotEmpty) + ListTile( + title: Text(AppLocalizations.of(context).description), + subtitle: Text(metadata.description), + ), + if (metadata.directory.isNotEmpty) + ListTile( + title: Text(AppLocalizations.of(context).directory), + subtitle: Text(metadata.directory), + ), + ListTile( + title: Text(AppLocalizations.of(context).tools), + subtitle: Wrap( + alignment: WrapAlignment.center, + children: [ + for (final tool in info?.tools ?? []) + SizedBox.square( + dimension: 64, + child: OptionButton( + icon: + Icon(tool.icon(PhosphorIconsStyle.light)), + tooltip: tool.name, + ), + ), + ], + ), + ), + ]), + )), + ) + ], + ); + }, + ), MenuItemButton( leadingIcon: const PhosphorIcon(PhosphorIconsLight.trash), child: Text(AppLocalizations.of(context).delete), @@ -436,7 +520,7 @@ class _TemplateItem extends StatelessWidget { ), ], onTap: () => openNewDocument( - context, replace, template, fileSystem.storage?.identifier), + context, bloc != null, template, fileSystem.storage?.identifier), ); } } diff --git a/app/lib/views/files/card.dart b/app/lib/views/files/card.dart index aa85d5979bea..1bf43f76b548 100644 --- a/app/lib/views/files/card.dart +++ b/app/lib/views/files/card.dart @@ -27,7 +27,7 @@ class AssetCard extends StatelessWidget { return ConstrainedBox( constraints: BoxConstraints(maxHeight: height), child: AspectRatio( - aspectRatio: 16 / 9, + aspectRatio: kThumbnailRatio, child: Card( elevation: 5, clipBehavior: Clip.hardEdge, diff --git a/metadata/en-US/changelogs/124.txt b/metadata/en-US/changelogs/124.txt index e276b67d857b..fb7f7f926a8c 100644 --- a/metadata/en-US/changelogs/124.txt +++ b/metadata/en-US/changelogs/124.txt @@ -1,3 +1,10 @@ +* Improve template dialog + * Add override tools in context menu + * Add information button + * Add duplicate button + * Change create template button to save icon + * Improve thumbnail display to have rounded corners +* Change zoom dependent to false by default * Fix zoom dependent not working correctly with the label tool ([#765](https://github.com/LinwoodDev/Butterfly/issues/765)) Read more here: https://linwood.dev/butterfly/2.2.3-rc.1 \ No newline at end of file