zefyr

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746
  1. // Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file
  2. // for details. All rights reserved. Use of this source code is governed by a
  3. // BSD-style license that can be found in the LICENSE file.
  4. import 'dart:async';
  5. import 'dart:io';
  6. import 'package:flutter/foundation.dart';
  7. import 'package:flutter/material.dart';
  8. import 'package:flutter/services.dart';
  9. import 'package:notus/notus.dart';
  10. import 'package:image_picker/image_picker.dart';
  11. import 'package:url_launcher/url_launcher.dart';
  12. import 'package:zefyr/src/extended_assets_picker/ct_asset_picker.dart';
  13. import 'link.dart';
  14. import 'scope.dart';
  15. import 'theme.dart';
  16. import 'toolbar.dart';
  17. const kToolbarButtonIcons = [
  18. ZefyrToolbarAction.text,
  19. ZefyrToolbarAction.heading,
  20. ZefyrToolbarAction.emoji,
  21. ZefyrToolbarAction.image,
  22. ZefyrToolbarAction.link,
  23. ZefyrToolbarAction.undo,
  24. ZefyrToolbarAction.redo,
  25. ZefyrToolbarAction.save,
  26. ZefyrToolbarAction.hideKeyboard,
  27. ZefyrToolbarAction.showKeyboard,
  28. ZefyrToolbarAction.close,
  29. ZefyrToolbarAction.confirm,
  30. ];
  31. /// A button used in [ZefyrToolbar].
  32. ///
  33. /// Create an instance of this widget with [ZefyrButton.icon] or
  34. /// [ZefyrButton.text] constructors.
  35. ///
  36. /// Toolbar buttons are normally created by a [ZefyrToolbarDelegate].
  37. class ZefyrButton extends StatelessWidget {
  38. /// Creates a toolbar button with an icon.
  39. ZefyrButton.icon({
  40. @required this.action,
  41. @required IconData icon,
  42. double iconSize,
  43. this.onPressed,
  44. }) : assert(action != null),
  45. assert(icon != null),
  46. _icon = icon,
  47. _iconSize = iconSize,
  48. _text = null,
  49. _textStyle = null,
  50. super();
  51. /// Creates a toolbar button containing text.
  52. ///
  53. /// Note that [ZefyrButton] has fixed width and does not expand to accommodate
  54. /// long texts.
  55. ZefyrButton.text({
  56. @required this.action,
  57. @required String text,
  58. TextStyle style,
  59. this.onPressed,
  60. }) : assert(action != null),
  61. assert(text != null),
  62. _icon = null,
  63. _iconSize = null,
  64. _text = text,
  65. _textStyle = style,
  66. super();
  67. /// Toolbar action associated with this button.
  68. final ZefyrToolbarAction action;
  69. final IconData _icon;
  70. final double _iconSize;
  71. final String _text;
  72. final TextStyle _textStyle;
  73. /// Callback to trigger when this button is tapped.
  74. final VoidCallback onPressed;
  75. bool get isAttributeAction {
  76. return kZefyrToolbarAttributeActions.keys.contains(action);
  77. }
  78. @override
  79. Widget build(BuildContext context) {
  80. final toolbar = ZefyrToolbar.of(context);
  81. final editor = toolbar.editor;
  82. final toolbarTheme = ZefyrTheme.of(context).toolbarTheme;
  83. final pressedHandler = _getPressedHandler(editor, toolbar);
  84. if (_icon != null) {
  85. return RawZefyrButton.icon(
  86. action: action,
  87. icon: _icon,
  88. size: 24,
  89. iconColor: (pressedHandler == null)
  90. ? toolbarTheme.disabledIconColor
  91. : _getColor(editor, toolbarTheme),
  92. color: _getFillColor(editor, toolbarTheme),
  93. onPressed: _getPressedHandler(editor, toolbar),
  94. );
  95. } else {
  96. assert(_text != null);
  97. var style = _textStyle ?? TextStyle();
  98. style = style.copyWith(
  99. color: _getColor(editor, toolbarTheme),
  100. );
  101. return RawZefyrButton(
  102. action: action,
  103. child: Text(_text, style: style),
  104. color: _getFillColor(editor, toolbarTheme),
  105. onPressed: _getPressedHandler(editor, toolbar),
  106. );
  107. }
  108. }
  109. Color _getColor(ZefyrScope editor, ToolbarTheme theme) {
  110. if (kToolbarButtonIcons.contains(action)) {
  111. return editor.toolbarAction == action
  112. ? theme.toggleColor
  113. : theme.surfaceColor;
  114. } else {
  115. if (isAttributeAction) {
  116. final attribute = kZefyrToolbarAttributeActions[action];
  117. final isToggled = (attribute is NotusAttribute)
  118. ? editor.selectionStyle.containsSame(attribute)
  119. : editor.selectionStyle.contains(attribute);
  120. return isToggled ? theme.toggleColor : theme.iconColor;
  121. }
  122. return null;
  123. }
  124. }
  125. Color _getFillColor(ZefyrScope editor, ToolbarTheme theme) {
  126. if (kToolbarButtonIcons.contains(action)) {
  127. return theme.color;
  128. } else {
  129. if (isAttributeAction) {
  130. final attribute = kZefyrToolbarAttributeActions[action];
  131. final isToggled = (attribute is NotusAttribute)
  132. ? editor.selectionStyle.containsSame(attribute)
  133. : editor.selectionStyle.contains(attribute);
  134. return isToggled ? Color(0xFFE1F5FF) : theme.iconFillColor;
  135. }
  136. return theme.iconFillColor;
  137. }
  138. }
  139. VoidCallback _getPressedHandler(
  140. ZefyrScope editor, ZefyrToolbarState toolbar) {
  141. if (onPressed != null) {
  142. return onPressed;
  143. } else if (isAttributeAction) {
  144. final attribute = kZefyrToolbarAttributeActions[action];
  145. if (attribute is NotusAttribute) {
  146. return () => _toggleAttribute(attribute, editor);
  147. }
  148. } else if (action == ZefyrToolbarAction.close) {
  149. return () => toolbar.closeOverlay();
  150. } else if (action == ZefyrToolbarAction.hideKeyboard) {
  151. return () => editor.closeKeyboard();
  152. } else if (action == ZefyrToolbarAction.showKeyboard) {
  153. return () => toolbar.closeOverlay();
  154. } else if (action == ZefyrToolbarAction.redo && editor.controller.document.history.stack.redo.isNotEmpty) {
  155. return () => editor.redo();
  156. } else if (action == ZefyrToolbarAction.undo && editor.controller.document.history.stack.undo.isNotEmpty) {
  157. return () => editor.undo();
  158. } else if (action == ZefyrToolbarAction.save) {
  159. return () => editor.onSave();
  160. }
  161. return null;
  162. }
  163. void _toggleAttribute(NotusAttribute attribute, ZefyrScope editor) {
  164. final isToggled = editor.selectionStyle.containsSame(attribute);
  165. if (isToggled) {
  166. editor.formatSelection(attribute.unset);
  167. } else {
  168. editor.formatSelection(attribute);
  169. }
  170. }
  171. }
  172. /// Raw button widget used by [ZefyrToolbar].
  173. ///
  174. /// See also:
  175. ///
  176. /// * [ZefyrButton], which wraps this widget and implements most of the
  177. /// action-specific logic.
  178. class RawZefyrButton extends StatelessWidget {
  179. const RawZefyrButton({
  180. Key key,
  181. @required this.action,
  182. @required this.child,
  183. @required this.color,
  184. @required this.onPressed,
  185. }) : super(key: key);
  186. /// Creates a [RawZefyrButton] containing an icon.
  187. RawZefyrButton.icon({
  188. @required this.action,
  189. @required IconData icon,
  190. double size,
  191. Color iconColor,
  192. @required this.color,
  193. @required this.onPressed,
  194. }) : child = Icon(icon, size: size, color: iconColor),
  195. super();
  196. /// Toolbar action associated with this button.
  197. final ZefyrToolbarAction action;
  198. /// Child widget to show inside this button. Usually an icon.
  199. final Widget child;
  200. /// Background color of this button.
  201. final Color color;
  202. /// Callback to trigger when this button is pressed.
  203. final VoidCallback onPressed;
  204. /// Returns `true` if this button is currently toggled on.
  205. bool get isToggled => color != null;
  206. @override
  207. Widget build(BuildContext context) {
  208. final theme = Theme.of(context);
  209. final isToolbarButton = kToolbarButtonIcons.contains(action);
  210. var constraints = theme.buttonTheme.constraints.copyWith(
  211. minWidth: 64,
  212. minHeight: 40,
  213. maxHeight: 40,
  214. );
  215. var radius = BorderRadius.all(Radius.circular(0));
  216. var padding = EdgeInsets.symmetric(horizontal: 1.0, vertical: 5.0);
  217. if (isToolbarButton) {
  218. constraints = theme.buttonTheme.constraints.copyWith(
  219. minWidth: 40,
  220. maxWidth: 40,
  221. minHeight: 30,
  222. maxHeight: 30,
  223. );
  224. padding = EdgeInsets.symmetric(horizontal: 0.0, vertical: 10);
  225. } else {
  226. if (action == ZefyrToolbarAction.headingLevel1 ||
  227. action == ZefyrToolbarAction.bold ||
  228. action == ZefyrToolbarAction.numberList) {
  229. radius = BorderRadius.horizontal(left: Radius.circular(4));
  230. }
  231. if (action == ZefyrToolbarAction.headingLevel6 ||
  232. action == ZefyrToolbarAction.deleteline ||
  233. action == ZefyrToolbarAction.bulletList) {
  234. radius = BorderRadius.horizontal(right: Radius.circular(4));
  235. }
  236. if (action == ZefyrToolbarAction.quote ||
  237. action == ZefyrToolbarAction.horizontalRule ||
  238. action == ZefyrToolbarAction.code) {
  239. radius = BorderRadius.all(Radius.circular(4));
  240. if (action == ZefyrToolbarAction.code ||
  241. action == ZefyrToolbarAction.quote) {
  242. padding = padding.copyWith(left: 12.0);
  243. }
  244. }
  245. }
  246. // Widget button = Row(
  247. // children: <Widget>[
  248. // // if (action == ZefyrToolbarAction.showKeyboard || action == ZefyrToolbarAction.hideKeyboard || action == ZefyrToolbarAction.link)
  249. // // Container(
  250. // // width: 1,
  251. // // color: Color(0xFFE2E2E2),
  252. // // height: 28,
  253. // // margin: EdgeInsets.symmetric(horizontal: 10),
  254. // // ),
  255. // ],
  256. // );
  257. return Padding(
  258. padding: padding,
  259. child: ConstrainedBox(
  260. constraints: constraints,
  261. child: RawMaterialButton(
  262. shape: RoundedRectangleBorder(borderRadius: radius),
  263. elevation: 0.0,
  264. fillColor: color,
  265. constraints: constraints,
  266. onPressed: onPressed,
  267. child: child,
  268. ),
  269. ),
  270. );
  271. //return isToolbarButton && action != ZefyrToolbarAction.showKeyboard && action != ZefyrToolbarAction.hideKeyboard ? Expanded(child: button) : button;
  272. }
  273. }
  274. /// Controls heading styles.
  275. ///
  276. /// When pressed, this button displays overlay toolbar with three
  277. /// buttons for each heading level.
  278. class HeadingButton extends StatefulWidget {
  279. const HeadingButton({Key key}) : super(key: key);
  280. @override
  281. _HeadingButtonState createState() => _HeadingButtonState();
  282. }
  283. class _HeadingButtonState extends State<HeadingButton> {
  284. @override
  285. Widget build(BuildContext context) {
  286. final toolbar = ZefyrToolbar.of(context);
  287. return toolbar.buildButton(
  288. context,
  289. ZefyrToolbarAction.heading,
  290. onPressed: showOverlay,
  291. );
  292. }
  293. void showOverlay() {
  294. final toolbar = ZefyrToolbar.of(context);
  295. toolbar.showOverlay(buildOverlay, ZefyrToolbarAction.heading);
  296. }
  297. Widget _buildButtonsView(List<Widget> buttons) {
  298. return Container(
  299. height: 52,
  300. child: ListView(
  301. scrollDirection: Axis.horizontal,
  302. children: buttons,
  303. physics: ClampingScrollPhysics(),
  304. ),
  305. );
  306. }
  307. bool hasColor(NotusStyle style, String color) {
  308. if (style.contains(NotusAttribute.color)) {
  309. var _style = style.get(NotusAttribute.color);
  310. return _style.value.toLowerCase() == color.toLowerCase();
  311. }
  312. return false;
  313. }
  314. Widget buildOverlay(BuildContext context) {
  315. final theme = ZefyrTheme.of(context).toolbarTheme;
  316. final toolbar = ZefyrToolbar.of(context);
  317. final headingButtons = <Widget>[
  318. toolbar.buildButton(context, ZefyrToolbarAction.headingLevel1),
  319. toolbar.buildButton(context, ZefyrToolbarAction.headingLevel2),
  320. toolbar.buildButton(context, ZefyrToolbarAction.headingLevel3),
  321. toolbar.buildButton(context, ZefyrToolbarAction.headingLevel4),
  322. toolbar.buildButton(context, ZefyrToolbarAction.headingLevel5),
  323. toolbar.buildButton(context, ZefyrToolbarAction.headingLevel6),
  324. ];
  325. final textButtons = <Widget>[
  326. toolbar.buildButton(context, ZefyrToolbarAction.bold),
  327. toolbar.buildButton(context, ZefyrToolbarAction.italic),
  328. toolbar.buildButton(context, ZefyrToolbarAction.underline),
  329. toolbar.buildButton(context, ZefyrToolbarAction.deleteline),
  330. toolbar.buildButton(context, ZefyrToolbarAction.quote),
  331. ];
  332. final listButtons = <Widget>[
  333. toolbar.buildButton(context, ZefyrToolbarAction.numberList),
  334. toolbar.buildButton(context, ZefyrToolbarAction.bulletList),
  335. ];
  336. final otherButtons = <Widget>[
  337. toolbar.buildButton(context, ZefyrToolbarAction.horizontalRule),
  338. toolbar.buildButton(context, ZefyrToolbarAction.code),
  339. ];
  340. final textColors = [
  341. ZefyrTheme.of(context).defaultLineTheme.textStyle.color, ...List.of(kTextColors),
  342. ];
  343. return Material(
  344. color: theme.color,
  345. child: Container(
  346. decoration: BoxDecoration(
  347. border: Border(top: BorderSide(color: theme.dividerColor, width: 1)),
  348. ),
  349. child: SingleChildScrollView(
  350. physics: ClampingScrollPhysics(),
  351. child: Column(
  352. children: [
  353. Container(
  354. padding: EdgeInsets.symmetric(vertical: 10, horizontal: 15),
  355. child: Column(
  356. children: [
  357. _buildButtonsView(headingButtons),
  358. _buildButtonsView(textButtons),
  359. _buildButtonsView(listButtons),
  360. _buildButtonsView(otherButtons),
  361. ],
  362. ),
  363. ),
  364. Container(
  365. decoration: BoxDecoration(
  366. border: Border(
  367. top: BorderSide(color: theme.dividerColor, width: 1)),
  368. ),
  369. padding: EdgeInsets.symmetric(vertical: 16),
  370. margin: EdgeInsets.symmetric(horizontal: 16),
  371. child: Row(
  372. children: <Widget>[
  373. Padding(
  374. padding: EdgeInsets.only(right: 12),
  375. child: Text(
  376. '文字色',
  377. style: Theme.of(context).textTheme.caption.copyWith(
  378. color: Theme.of(context).colorScheme.onSurface,
  379. fontSize: 14,
  380. fontWeight: FontWeight.w500,
  381. ),
  382. ),
  383. ),
  384. Expanded(
  385. child: Container(
  386. height: 20,
  387. child: ListView(
  388. scrollDirection: Axis.horizontal,
  389. children: textColors.map((color) {
  390. var hex = color.value.toRadixString(16).substring(2, 8);
  391. var has = false;
  392. if (!toolbar.editor.selectionStyle.contains(NotusAttribute.color)) {
  393. if (color == ZefyrTheme.of(context).defaultLineTheme.textStyle.color) {
  394. has = true;
  395. }
  396. } else {
  397. has = hasColor(toolbar.editor.selectionStyle, '#$hex');
  398. }
  399. // var key = kTextColors[index].value;
  400. return GestureDetector(
  401. behavior: HitTestBehavior.translucent,
  402. onTap: () {
  403. toolbar.editor.formatSelection(NotusAttribute.color.fromString('#$hex'));
  404. },
  405. child: Container(
  406. padding: EdgeInsets.all(1),
  407. width: 20,
  408. height: 20,
  409. decoration: ShapeDecoration(
  410. shape: CircleBorder(
  411. side: has ? BorderSide(
  412. width: 1,
  413. color: theme.toggleColor,
  414. ) : BorderSide.none,
  415. ),
  416. ),
  417. margin: EdgeInsets.symmetric(horizontal: 8),
  418. child: Container(
  419. // width: 16,
  420. // height: 16,
  421. decoration: ShapeDecoration(
  422. color: color,
  423. shape: CircleBorder(
  424. side: color == Color(0xFFFFFFFF) && !has ? BorderSide(
  425. width: 1,
  426. color: theme.dividerColor,
  427. ) : BorderSide.none,
  428. ),
  429. ),
  430. ),
  431. ),
  432. );
  433. }).toList(),
  434. physics: ClampingScrollPhysics(),
  435. ),
  436. ),
  437. ),
  438. ],
  439. ),
  440. ),
  441. ],
  442. ),
  443. ),
  444. ),
  445. );
  446. // ZefyrToolbarScaffold(body: Container(
  447. // height: 200,
  448. // child: buttons,
  449. // ));
  450. }
  451. }
  452. // class TextColorButton extends StatelessWidget {
  453. // const TextColorButton({Key key, String color, ZefyrThemeData theme}) : super(key: key);
  454. // @override
  455. // Widget build(BuildContext context) {
  456. // return GestureDetector(
  457. // behavior: HitTestBehavior.translucent,
  458. // onTap: () {
  459. // // onTap(color);
  460. // // toolbar.editor.formatSelection(NotusAttribute.color.fromString('#000000'));
  461. // },
  462. // child: Container(
  463. // padding: EdgeInsets.all(1),
  464. // decoration: ShapeDecoration(
  465. // shape: CircleBorder(
  466. // side: BorderSide(
  467. // width: 1,
  468. // color: theme,
  469. // ),
  470. // ),
  471. // ),
  472. // margin: EdgeInsets.symmetric(horizontal: 8),
  473. // child: Container(
  474. // width: 16,
  475. // height: 16,
  476. // decoration: ShapeDecoration(
  477. // color: ZefyrTheme.of(context)
  478. // .defaultLineTheme
  479. // .textStyle
  480. // .color,
  481. // shape: CircleBorder(),
  482. // ),
  483. // ),
  484. // ),
  485. // );
  486. // }
  487. // }
  488. /// Controls image attribute.
  489. ///
  490. /// When pressed, this button displays overlay toolbar with three
  491. /// buttons for each heading level.
  492. class ImageButton extends StatefulWidget {
  493. const ImageButton({Key key}) : super(key: key);
  494. @override
  495. _ImageButtonState createState() => _ImageButtonState();
  496. }
  497. class _ImageButtonState extends State<ImageButton> {
  498. final provider = ExtendedAssetPickerProvider(
  499. maxAssets: 9,
  500. pageSize: 320,
  501. pathThumbSize: 200,
  502. selectedAssets: [],
  503. requestType: RequestType.image,
  504. sortPathDelegate: CustomSortPathDelegate(),
  505. routeDuration: const Duration(milliseconds: 300),
  506. );
  507. @override
  508. Widget build(BuildContext context) {
  509. final toolbar = ZefyrToolbar.of(context);
  510. return toolbar.buildButton(
  511. context,
  512. ZefyrToolbarAction.image,
  513. onPressed: showOverlay,
  514. );
  515. }
  516. Future<void> showOverlay() async {
  517. final toolbar = ZefyrToolbar.of(context);
  518. if (Platform.isIOS || Platform.isAndroid) {
  519. var isPermissionGranted = await PhotoManager.requestPermission();
  520. if (isPermissionGranted) {
  521. return toolbar.showOverlay(buildOverlay, ZefyrToolbarAction.image);
  522. } else {
  523. PhotoManager.openSetting();
  524. }
  525. } else {
  526. print('打开file');
  527. }
  528. }
  529. Widget buildOverlay(BuildContext context) {
  530. return ExtendedAssetPicker.buildQuickPicker(
  531. context,
  532. provider: provider,
  533. );
  534. }
  535. }
  536. class LinkButton extends StatefulWidget {
  537. const LinkButton({Key key}) : super(key: key);
  538. @override
  539. _LinkButtonState createState() => _LinkButtonState();
  540. }
  541. class _LinkButtonState extends State<LinkButton> {
  542. final TextEditingController _inputController = TextEditingController();
  543. Key _inputKey;
  544. bool _formatError = false;
  545. bool get isEditing => _inputKey != null;
  546. @override
  547. Widget build(BuildContext context) {
  548. final toolbar = ZefyrToolbar.of(context);
  549. final editor = toolbar.editor;
  550. final enabled =
  551. hasLink(editor.selectionStyle) || !editor.selection.isCollapsed;
  552. return toolbar.buildButton(
  553. context,
  554. ZefyrToolbarAction.link,
  555. // onPressed: enabled ? showOverlay : null,
  556. onPressed: showOverlay,
  557. );
  558. }
  559. bool hasLink(NotusStyle style) => style.contains(NotusAttribute.link);
  560. String getLink([String defaultValue = '']) {
  561. final editor = ZefyrToolbar.of(context).editor;
  562. final attrs = editor.selectionStyle;
  563. if (hasLink(attrs)) {
  564. return attrs.value(NotusAttribute.link);
  565. }
  566. return defaultValue;
  567. }
  568. String getText([String defaultValue = '']) {
  569. final editor = ZefyrToolbar.of(context).editor;
  570. final plainTextEditingValue = editor.controller.plainTextEditingValue;
  571. if (!editor.selection.isCollapsed) {
  572. return plainTextEditingValue.text.substring(editor.selection.start, editor.selection.end);
  573. }
  574. return defaultValue;
  575. }
  576. OverlayEntry _overlayEntry;
  577. Future<void> showOverlay() async {
  578. final editor = ZefyrToolbar.of(context).editor;
  579. editor.closeKeyboard(true);
  580. var _selection = editor.selection;
  581. var result = await editor.linkDelegate.fillLink(context, ZefyrLinkEntity(
  582. text: getText(),
  583. url: getLink(),
  584. ));
  585. if (result != null) {
  586. // toolbar.editor.updateSelection(_selection, source: ChangeSource.local);
  587. // _selection = toolbar.editor.selection;
  588. editor.controller.replaceText(
  589. _selection.start, _selection.end - _selection.start, result.text,
  590. selection: _selection.copyWith(
  591. baseOffset: _selection.start,
  592. extentOffset: _selection.start + result.text.length,
  593. ));
  594. editor.formatSelection(NotusAttribute.link.fromString(result.url));
  595. editor.closeKeyboard();
  596. } else {
  597. editor.updateSelection(_selection, source: ChangeSource.local);
  598. }
  599. }
  600. void closeOverlay() {
  601. final toolbar = ZefyrToolbar.of(context);
  602. toolbar.closeOverlay();
  603. }
  604. void edit() {
  605. final toolbar = ZefyrToolbar.of(context);
  606. setState(() {
  607. _inputKey = UniqueKey();
  608. _inputController.text = getLink('https://');
  609. _inputController.addListener(_handleInputChange);
  610. toolbar.markNeedsRebuild();
  611. });
  612. }
  613. void doneEdit() {
  614. final toolbar = ZefyrToolbar.of(context);
  615. setState(() {
  616. var error = false;
  617. if (_inputController.text.isNotEmpty) {
  618. try {
  619. var uri = Uri.parse(_inputController.text);
  620. if ((uri.isScheme('https') || uri.isScheme('http')) &&
  621. uri.host.isNotEmpty) {
  622. toolbar.editor.formatSelection(
  623. NotusAttribute.link.fromString(_inputController.text));
  624. } else {
  625. error = true;
  626. }
  627. } on FormatException {
  628. error = true;
  629. }
  630. }
  631. if (error) {
  632. _formatError = error;
  633. toolbar.markNeedsRebuild();
  634. } else {
  635. _inputKey = null;
  636. _inputController.text = '';
  637. _inputController.removeListener(_handleInputChange);
  638. toolbar.markNeedsRebuild();
  639. toolbar.editor.focus();
  640. }
  641. });
  642. }
  643. void cancelEdit() {
  644. if (mounted) {
  645. final editor = ZefyrToolbar.of(context).editor;
  646. setState(() {
  647. _inputKey = null;
  648. _inputController.text = '';
  649. _inputController.removeListener(_handleInputChange);
  650. editor.focus();
  651. });
  652. }
  653. }
  654. void unlink() {
  655. final editor = ZefyrToolbar.of(context).editor;
  656. editor.formatSelection(NotusAttribute.link.unset);
  657. closeOverlay();
  658. }
  659. void copyToClipboard() {
  660. var link = getLink();
  661. assert(link != null);
  662. Clipboard.setData(ClipboardData(text: link));
  663. }
  664. void openInBrowser() async {
  665. final editor = ZefyrToolbar.of(context).editor;
  666. var link = getLink();
  667. assert(link != null);
  668. if (await canLaunch(link)) {
  669. editor.closeKeyboard();
  670. await launch(link, forceWebView: true);
  671. }
  672. }
  673. void _handleInputChange() {
  674. final toolbar = ZefyrToolbar.of(context);
  675. setState(() {
  676. _formatError = false;
  677. toolbar.markNeedsRebuild();
  678. });
  679. }
  680. }