zefyr

buttons.dart 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937
  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:io';
  5. import 'dart:typed_data';
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter/services.dart';
  8. import 'package:notus/notus.dart';
  9. import 'package:photo/photo.dart';
  10. import 'package:photo_manager/photo_manager.dart';
  11. import 'package:url_launcher/url_launcher.dart';
  12. import 'scope.dart';
  13. import 'theme.dart';
  14. import 'toolbar.dart';
  15. const kToolbarButtonIcons = [
  16. ZefyrToolbarAction.text,
  17. ZefyrToolbarAction.heading,
  18. ZefyrToolbarAction.emoji,
  19. ZefyrToolbarAction.image,
  20. ZefyrToolbarAction.link,
  21. ZefyrToolbarAction.hideKeyboard,
  22. ZefyrToolbarAction.showKeyboard,
  23. ZefyrToolbarAction.close,
  24. ZefyrToolbarAction.confirm,
  25. ];
  26. /// A button used in [ZefyrToolbar].
  27. ///
  28. /// Create an instance of this widget with [ZefyrButton.icon] or
  29. /// [ZefyrButton.text] constructors.
  30. ///
  31. /// Toolbar buttons are normally created by a [ZefyrToolbarDelegate].
  32. class ZefyrButton extends StatelessWidget {
  33. /// Creates a toolbar button with an icon.
  34. ZefyrButton.icon({
  35. @required this.action,
  36. @required IconData icon,
  37. double iconSize,
  38. this.onPressed,
  39. }) : assert(action != null),
  40. assert(icon != null),
  41. _icon = icon,
  42. _iconSize = iconSize,
  43. _text = null,
  44. _textStyle = null,
  45. super();
  46. /// Creates a toolbar button containing text.
  47. ///
  48. /// Note that [ZefyrButton] has fixed width and does not expand to accommodate
  49. /// long texts.
  50. ZefyrButton.text({
  51. @required this.action,
  52. @required String text,
  53. TextStyle style,
  54. this.onPressed,
  55. }) : assert(action != null),
  56. assert(text != null),
  57. _icon = null,
  58. _iconSize = null,
  59. _text = text,
  60. _textStyle = style,
  61. super();
  62. /// Toolbar action associated with this button.
  63. final ZefyrToolbarAction action;
  64. final IconData _icon;
  65. final double _iconSize;
  66. final String _text;
  67. final TextStyle _textStyle;
  68. /// Callback to trigger when this button is tapped.
  69. final VoidCallback onPressed;
  70. bool get isAttributeAction {
  71. return kZefyrToolbarAttributeActions.keys.contains(action);
  72. }
  73. @override
  74. Widget build(BuildContext context) {
  75. final toolbar = ZefyrToolbar.of(context);
  76. final editor = toolbar.editor;
  77. final toolbarTheme = ZefyrTheme.of(context).toolbarTheme;
  78. final pressedHandler = _getPressedHandler(editor, toolbar);
  79. final iconColor = (pressedHandler == null)
  80. ? toolbarTheme.disabledIconColor
  81. : toolbarTheme.iconColor;
  82. if (_icon != null) {
  83. return RawZefyrButton.icon(
  84. action: action,
  85. icon: _icon,
  86. size: _iconSize,
  87. iconColor: _getColor(editor, toolbarTheme),
  88. color: _getFillColor(action, toolbarTheme),
  89. onPressed: _getPressedHandler(editor, toolbar),
  90. );
  91. } else {
  92. assert(_text != null);
  93. var style = _textStyle ?? TextStyle();
  94. style = style.copyWith(
  95. color: _getColor(editor, toolbarTheme),
  96. );
  97. return RawZefyrButton(
  98. action: action,
  99. child: Text(_text, style: style),
  100. color: _getFillColor(action, toolbarTheme),
  101. onPressed: _getPressedHandler(editor, toolbar),
  102. );
  103. }
  104. }
  105. Color _getColor(ZefyrScope editor, ToolbarTheme theme) {
  106. if (isAttributeAction) {
  107. final attribute = kZefyrToolbarAttributeActions[action];
  108. final isToggled = (attribute is NotusAttribute)
  109. ? editor.selectionStyle.containsSame(attribute)
  110. : editor.selectionStyle.contains(attribute);
  111. return isToggled ? theme.toggleColor : null;
  112. }
  113. return null;
  114. }
  115. Color _getFillColor(ZefyrToolbarAction action, ToolbarTheme theme) {
  116. if (kToolbarButtonIcons.contains(action)) {
  117. return theme.color;
  118. }
  119. return theme.iconFillColor;
  120. }
  121. VoidCallback _getPressedHandler(
  122. ZefyrScope editor, ZefyrToolbarState toolbar) {
  123. if (onPressed != null) {
  124. return onPressed;
  125. } else if (isAttributeAction) {
  126. final attribute = kZefyrToolbarAttributeActions[action];
  127. if (attribute is NotusAttribute) {
  128. return () => _toggleAttribute(attribute, editor);
  129. }
  130. } else if (action == ZefyrToolbarAction.close) {
  131. return () => toolbar.closeOverlay();
  132. } else if (action == ZefyrToolbarAction.hideKeyboard) {
  133. return () => editor.closeKeyboard();
  134. } else if (action == ZefyrToolbarAction.showKeyboard) {
  135. return () => toolbar.closeOverlay();
  136. }
  137. return null;
  138. }
  139. void _toggleAttribute(NotusAttribute attribute, ZefyrScope editor) {
  140. final isToggled = editor.selectionStyle.containsSame(attribute);
  141. if (isToggled) {
  142. editor.formatSelection(attribute.unset);
  143. } else {
  144. editor.formatSelection(attribute);
  145. }
  146. }
  147. }
  148. /// Raw button widget used by [ZefyrToolbar].
  149. ///
  150. /// See also:
  151. ///
  152. /// * [ZefyrButton], which wraps this widget and implements most of the
  153. /// action-specific logic.
  154. class RawZefyrButton extends StatelessWidget {
  155. const RawZefyrButton({
  156. Key key,
  157. @required this.action,
  158. @required this.child,
  159. @required this.color,
  160. @required this.onPressed,
  161. }) : super(key: key);
  162. /// Creates a [RawZefyrButton] containing an icon.
  163. RawZefyrButton.icon({
  164. @required this.action,
  165. @required IconData icon,
  166. double size,
  167. Color iconColor,
  168. @required this.color,
  169. @required this.onPressed,
  170. }) : child = Icon(icon, size: size, color: iconColor),
  171. super();
  172. /// Toolbar action associated with this button.
  173. final ZefyrToolbarAction action;
  174. /// Child widget to show inside this button. Usually an icon.
  175. final Widget child;
  176. /// Background color of this button.
  177. final Color color;
  178. /// Callback to trigger when this button is pressed.
  179. final VoidCallback onPressed;
  180. /// Returns `true` if this button is currently toggled on.
  181. bool get isToggled => color != null;
  182. @override
  183. Widget build(BuildContext context) {
  184. final theme = Theme.of(context);
  185. final constraints = theme.buttonTheme.constraints.copyWith(
  186. minWidth: 64,
  187. minHeight: 40,
  188. maxHeight: 40,
  189. );
  190. var radius = BorderRadius.all(Radius.circular(0));
  191. if (action == ZefyrToolbarAction.headingLevel1 ||
  192. action == ZefyrToolbarAction.bold ||
  193. action == ZefyrToolbarAction.numberList) {
  194. radius = BorderRadius.horizontal(left: Radius.circular(4));
  195. }
  196. if (action == ZefyrToolbarAction.headingLevel6 ||
  197. action == ZefyrToolbarAction.deleteline ||
  198. action == ZefyrToolbarAction.bulletList) {
  199. radius = BorderRadius.horizontal(left: Radius.circular(4));
  200. }
  201. if (action == ZefyrToolbarAction.quote ||
  202. action == ZefyrToolbarAction.horizontalRule ||
  203. action == ZefyrToolbarAction.code) {
  204. radius = BorderRadius.all(Radius.circular(4));
  205. }
  206. return Padding(
  207. padding:
  208. const EdgeInsets.symmetric(horizontal: 1.0, vertical: 6.0).copyWith(
  209. left: action == ZefyrToolbarAction.code ||
  210. action == ZefyrToolbarAction.quote
  211. ? 12.0
  212. : 1.0,
  213. ),
  214. child: RawMaterialButton(
  215. shape: RoundedRectangleBorder(borderRadius: radius),
  216. elevation: 0.0,
  217. fillColor: color,
  218. constraints: constraints,
  219. onPressed: onPressed,
  220. child: child,
  221. ),
  222. );
  223. }
  224. }
  225. /// Controls heading styles.
  226. ///
  227. /// When pressed, this button displays overlay toolbar with three
  228. /// buttons for each heading level.
  229. class HeadingButton extends StatefulWidget {
  230. const HeadingButton({Key key}) : super(key: key);
  231. @override
  232. _HeadingButtonState createState() => _HeadingButtonState();
  233. }
  234. class _HeadingButtonState extends State<HeadingButton> {
  235. @override
  236. Widget build(BuildContext context) {
  237. final toolbar = ZefyrToolbar.of(context);
  238. return toolbar.buildButton(
  239. context,
  240. ZefyrToolbarAction.heading,
  241. onPressed: showOverlay,
  242. );
  243. }
  244. void showOverlay() {
  245. final toolbar = ZefyrToolbar.of(context);
  246. toolbar.showOverlay(buildOverlay);
  247. }
  248. Widget _buildButtonsView(List<Widget> buttons) {
  249. return Container(
  250. height: 52,
  251. child: ListView(
  252. scrollDirection: Axis.horizontal,
  253. children: buttons,
  254. physics: ClampingScrollPhysics(),
  255. ),
  256. );
  257. }
  258. Widget buildOverlay(BuildContext context) {
  259. final theme = ZefyrTheme.of(context).toolbarTheme;
  260. final toolbar = ZefyrToolbar.of(context);
  261. final headingButtons = <Widget>[
  262. toolbar.buildButton(context, ZefyrToolbarAction.headingLevel1),
  263. toolbar.buildButton(context, ZefyrToolbarAction.headingLevel2),
  264. toolbar.buildButton(context, ZefyrToolbarAction.headingLevel3),
  265. toolbar.buildButton(context, ZefyrToolbarAction.headingLevel4),
  266. toolbar.buildButton(context, ZefyrToolbarAction.headingLevel5),
  267. toolbar.buildButton(context, ZefyrToolbarAction.headingLevel6),
  268. ];
  269. final textButtons = <Widget>[
  270. toolbar.buildButton(context, ZefyrToolbarAction.bold),
  271. toolbar.buildButton(context, ZefyrToolbarAction.italic),
  272. toolbar.buildButton(context, ZefyrToolbarAction.underline),
  273. toolbar.buildButton(context, ZefyrToolbarAction.deleteline),
  274. toolbar.buildButton(context, ZefyrToolbarAction.quote),
  275. ];
  276. final listButtons = <Widget>[
  277. toolbar.buildButton(context, ZefyrToolbarAction.numberList),
  278. toolbar.buildButton(context, ZefyrToolbarAction.bulletList),
  279. ];
  280. final otherButtons = <Widget>[
  281. toolbar.buildButton(context, ZefyrToolbarAction.horizontalRule),
  282. toolbar.buildButton(context, ZefyrToolbarAction.code),
  283. ];
  284. return Material(
  285. color: theme.color,
  286. child: Container(
  287. decoration: BoxDecoration(
  288. border: Border(top: BorderSide(color: theme.dividerColor, width: 1)),
  289. ),
  290. child: Column(
  291. children: [
  292. Container(
  293. padding: EdgeInsets.symmetric(vertical: 10, horizontal: 15),
  294. child: Column(
  295. children: [
  296. _buildButtonsView(headingButtons),
  297. _buildButtonsView(textButtons),
  298. _buildButtonsView(listButtons),
  299. _buildButtonsView(otherButtons),
  300. ],
  301. ),
  302. ),
  303. Container(
  304. decoration: BoxDecoration(
  305. border: Border(
  306. top: BorderSide(color: theme.dividerColor, width: 1)),
  307. ),
  308. padding: EdgeInsets.symmetric(vertical: 16),
  309. margin: EdgeInsets.symmetric(horizontal: 16),
  310. child: Row(
  311. children: <Widget>[
  312. Padding(
  313. padding: EdgeInsets.only(right: 12),
  314. child: Text(
  315. '文字色',
  316. style: Theme.of(context).textTheme.caption.copyWith(
  317. color: Theme.of(context).colorScheme.onSurface,
  318. fontSize: 14,
  319. fontWeight: FontWeight.w500,
  320. ),
  321. ),
  322. ),
  323. Expanded(
  324. child: Container(
  325. height: 20,
  326. child: ListView(
  327. scrollDirection: Axis.horizontal,
  328. children: [
  329. Container(
  330. padding: EdgeInsets.all(1),
  331. decoration: ShapeDecoration(
  332. shape: CircleBorder(
  333. side: BorderSide(
  334. width: 1, color: theme.toggleColor),
  335. ),
  336. ),
  337. margin: EdgeInsets.symmetric(horizontal: 8),
  338. child: Container(
  339. width: 16,
  340. height: 16,
  341. decoration: ShapeDecoration(
  342. color: Color(0xFFE02020),
  343. shape: CircleBorder(),
  344. ),
  345. ),
  346. ),
  347. Container(
  348. padding: EdgeInsets.all(1),
  349. decoration: ShapeDecoration(
  350. shape: CircleBorder(
  351. side: BorderSide.none,
  352. ),
  353. ),
  354. margin: EdgeInsets.symmetric(horizontal: 8),
  355. child: Container(
  356. width: 16,
  357. height: 16,
  358. decoration: ShapeDecoration(
  359. color: Color(0xFFFA6400),
  360. shape: CircleBorder(),
  361. ),
  362. ),
  363. ),
  364. Container(
  365. padding: EdgeInsets.all(1),
  366. decoration: ShapeDecoration(
  367. shape: CircleBorder(
  368. side: BorderSide.none,
  369. ),
  370. ),
  371. margin: EdgeInsets.symmetric(horizontal: 8),
  372. child: Container(
  373. width: 16,
  374. height: 16,
  375. decoration: ShapeDecoration(
  376. color: Color(0xFF0091FF),
  377. shape: CircleBorder(),
  378. ),
  379. ),
  380. ),
  381. ],
  382. physics: ClampingScrollPhysics(),
  383. ),
  384. ),
  385. ),
  386. ],
  387. ),
  388. ),
  389. ],
  390. ),
  391. ),
  392. );
  393. // ZefyrToolbarScaffold(body: Container(
  394. // height: 200,
  395. // child: buttons,
  396. // ));
  397. }
  398. }
  399. /// Controls image attribute.
  400. ///
  401. /// When pressed, this button displays overlay toolbar with three
  402. /// buttons for each heading level.
  403. class ImageButton extends StatefulWidget {
  404. const ImageButton({Key key}) : super(key: key);
  405. @override
  406. _ImageButtonState createState() => _ImageButtonState();
  407. }
  408. class _ImageButtonState extends State<ImageButton> {
  409. @override
  410. Widget build(BuildContext context) {
  411. final toolbar = ZefyrToolbar.of(context);
  412. return toolbar.buildButton(
  413. context,
  414. ZefyrToolbarAction.image,
  415. onPressed: showOverlay,
  416. );
  417. }
  418. Future<void> showOverlay() async {
  419. final toolbar = ZefyrToolbar.of(context);
  420. if (Platform.isIOS || Platform.isAndroid) {
  421. var result = await PhotoManager.requestPermission();
  422. if (result) {
  423. return toolbar.showOverlay(buildOverlay);
  424. } else {
  425. PhotoManager.openSetting();
  426. }
  427. } else {
  428. print('打开file');
  429. }
  430. }
  431. Widget buildOverlay(BuildContext context) {
  432. final theme = ZefyrTheme.of(context).toolbarTheme;
  433. final toolbar = ZefyrToolbar.of(context);
  434. return Material(
  435. color: theme.color,
  436. child: Container(
  437. child: Column(
  438. children: <Widget>[
  439. Expanded(
  440. child: Container(
  441. decoration: BoxDecoration(
  442. border: Border(
  443. top: BorderSide(color: theme.dividerColor, width: 1),
  444. bottom: BorderSide(color: theme.dividerColor, width: 1),
  445. ),
  446. ),
  447. child: Row(
  448. children: [
  449. Container(
  450. width: 58,
  451. child: Column(
  452. children: [
  453. Expanded(
  454. child: FlatButton(
  455. shape: RoundedRectangleBorder(),
  456. color: Color(0xFFF6F6F6),
  457. onPressed: () {},
  458. child: Container(
  459. child: Column(
  460. mainAxisAlignment: MainAxisAlignment.center,
  461. children: [
  462. Icon(
  463. kDefaultButtonIcons[
  464. ZefyrToolbarAction.cameraImage],
  465. size: 24,
  466. color: Color(0xFFBFBFBF),
  467. ),
  468. Padding(
  469. padding: EdgeInsets.only(top: 6),
  470. child: Text(
  471. '拍照',
  472. style: TextStyle(
  473. fontSize: 12,
  474. color: Color(0xFF8C8C8C),
  475. ),
  476. ),
  477. )
  478. ],
  479. ),
  480. )),
  481. ),
  482. Expanded(
  483. child: FlatButton(
  484. shape: RoundedRectangleBorder(),
  485. color: Color(0xFFF6F6F6),
  486. onPressed: () async {
  487. List<AssetEntity> imgList = await PhotoPicker.pickAsset(
  488. context: context,
  489. rowCount: 4,
  490. itemRadio: 1,
  491. padding: 4,
  492. provider: I18nProvider.chinese,
  493. sortDelegate: SortDelegate.common,
  494. pickType: PickType.onlyImage,
  495. photoPathList: null,
  496. );
  497. print(imgList);
  498. },
  499. child: Container(
  500. child: Column(
  501. mainAxisAlignment: MainAxisAlignment.center,
  502. children: [
  503. Icon(
  504. kDefaultButtonIcons[
  505. ZefyrToolbarAction.galleryImage],
  506. size: 24,
  507. color: Color(0xFFBFBFBF),
  508. ),
  509. Padding(
  510. padding: EdgeInsets.only(top: 6),
  511. child: Text(
  512. '相册',
  513. style: TextStyle(
  514. fontSize: 12,
  515. color: Color(0xFF8C8C8C),
  516. ),
  517. ),
  518. )
  519. ],
  520. ),
  521. )),
  522. ),
  523. ],
  524. ),
  525. ),
  526. Expanded(
  527. child: PhotoPicker.buildGallery(
  528. context: context,
  529. itemWidth: (toolbar.keyboardHeight - 50) * 0.5,
  530. padding: 4,
  531. provider: I18nProvider.chinese,
  532. sortDelegate: SortDelegate.common,
  533. pickType: PickType.onlyImage,
  534. photoPathList: null,
  535. ),
  536. ),
  537. ],
  538. ),
  539. ),
  540. ),
  541. Container(
  542. height: 50,
  543. color: theme.color,
  544. padding: EdgeInsets.symmetric(horizontal: 20),
  545. child: Row(
  546. children: [
  547. Expanded(
  548. child: Row(
  549. children: [
  550. SizedBox(
  551. width: 16,
  552. height: 16,
  553. child: Radio<bool>(
  554. value: false,
  555. groupValue: false,
  556. onChanged: (bool result) {},
  557. ),
  558. ),
  559. Padding(
  560. padding: EdgeInsets.symmetric(horizontal: 8),
  561. child: Text(
  562. '原图',
  563. style:
  564. TextStyle(color: theme.iconColor, fontSize: 16),
  565. ),
  566. ),
  567. ],
  568. ),
  569. ),
  570. FlatButton(
  571. padding: EdgeInsets.zero,
  572. color: theme.toggleColor,
  573. shape: StadiumBorder(),
  574. onPressed: () {},
  575. child: Container(
  576. height: 30,
  577. alignment: Alignment.center,
  578. padding: EdgeInsets.symmetric(horizontal: 20),
  579. child: Text(
  580. '上传 (2)',
  581. style: TextStyle(
  582. color: Colors.white,
  583. fontSize: 14,
  584. ),
  585. ),
  586. ),
  587. ),
  588. ],
  589. ),
  590. )
  591. ],
  592. ),
  593. ),
  594. );
  595. }
  596. void _pickFromCamera() async {
  597. final editor = ZefyrToolbar.of(context).editor;
  598. final image =
  599. await editor.imageDelegate.pickImage(editor.imageDelegate.cameraSource);
  600. if (image != null) {
  601. editor.formatSelection(NotusAttribute.embed.image(image));
  602. }
  603. }
  604. void _pickFromGallery() async {
  605. final editor = ZefyrToolbar.of(context).editor;
  606. final image = await editor.imageDelegate
  607. .pickImage(editor.imageDelegate.gallerySource);
  608. if (image != null) {
  609. editor.formatSelection(NotusAttribute.embed.image(image));
  610. }
  611. }
  612. }
  613. class LinkButton extends StatefulWidget {
  614. const LinkButton({Key key}) : super(key: key);
  615. @override
  616. _LinkButtonState createState() => _LinkButtonState();
  617. }
  618. class _LinkButtonState extends State<LinkButton> {
  619. final TextEditingController _inputController = TextEditingController();
  620. Key _inputKey;
  621. bool _formatError = false;
  622. bool get isEditing => _inputKey != null;
  623. @override
  624. Widget build(BuildContext context) {
  625. final toolbar = ZefyrToolbar.of(context);
  626. final editor = toolbar.editor;
  627. final enabled =
  628. hasLink(editor.selectionStyle) || !editor.selection.isCollapsed;
  629. return toolbar.buildButton(
  630. context,
  631. ZefyrToolbarAction.link,
  632. onPressed: enabled ? showOverlay : null,
  633. );
  634. }
  635. bool hasLink(NotusStyle style) => style.contains(NotusAttribute.link);
  636. String getLink([String defaultValue]) {
  637. final editor = ZefyrToolbar.of(context).editor;
  638. final attrs = editor.selectionStyle;
  639. if (hasLink(attrs)) {
  640. return attrs.value(NotusAttribute.link);
  641. }
  642. return defaultValue;
  643. }
  644. void showOverlay() {
  645. final toolbar = ZefyrToolbar.of(context);
  646. toolbar.showOverlay(buildOverlay).whenComplete(cancelEdit);
  647. }
  648. void closeOverlay() {
  649. final toolbar = ZefyrToolbar.of(context);
  650. toolbar.closeOverlay();
  651. }
  652. void edit() {
  653. final toolbar = ZefyrToolbar.of(context);
  654. setState(() {
  655. _inputKey = UniqueKey();
  656. _inputController.text = getLink('https://');
  657. _inputController.addListener(_handleInputChange);
  658. toolbar.markNeedsRebuild();
  659. });
  660. }
  661. void doneEdit() {
  662. final toolbar = ZefyrToolbar.of(context);
  663. setState(() {
  664. var error = false;
  665. if (_inputController.text.isNotEmpty) {
  666. try {
  667. var uri = Uri.parse(_inputController.text);
  668. if ((uri.isScheme('https') || uri.isScheme('http')) &&
  669. uri.host.isNotEmpty) {
  670. toolbar.editor.formatSelection(
  671. NotusAttribute.link.fromString(_inputController.text));
  672. } else {
  673. error = true;
  674. }
  675. } on FormatException {
  676. error = true;
  677. }
  678. }
  679. if (error) {
  680. _formatError = error;
  681. toolbar.markNeedsRebuild();
  682. } else {
  683. _inputKey = null;
  684. _inputController.text = '';
  685. _inputController.removeListener(_handleInputChange);
  686. toolbar.markNeedsRebuild();
  687. toolbar.editor.focus();
  688. }
  689. });
  690. }
  691. void cancelEdit() {
  692. if (mounted) {
  693. final editor = ZefyrToolbar.of(context).editor;
  694. setState(() {
  695. _inputKey = null;
  696. _inputController.text = '';
  697. _inputController.removeListener(_handleInputChange);
  698. editor.focus();
  699. });
  700. }
  701. }
  702. void unlink() {
  703. final editor = ZefyrToolbar.of(context).editor;
  704. editor.formatSelection(NotusAttribute.link.unset);
  705. closeOverlay();
  706. }
  707. void copyToClipboard() {
  708. var link = getLink();
  709. assert(link != null);
  710. Clipboard.setData(ClipboardData(text: link));
  711. }
  712. void openInBrowser() async {
  713. final editor = ZefyrToolbar.of(context).editor;
  714. var link = getLink();
  715. assert(link != null);
  716. if (await canLaunch(link)) {
  717. editor.closeKeyboard();
  718. await launch(link, forceWebView: true);
  719. }
  720. }
  721. void _handleInputChange() {
  722. final toolbar = ZefyrToolbar.of(context);
  723. setState(() {
  724. _formatError = false;
  725. toolbar.markNeedsRebuild();
  726. });
  727. }
  728. Widget buildOverlay(BuildContext context) {
  729. final toolbar = ZefyrToolbar.of(context);
  730. final style = toolbar.editor.selectionStyle;
  731. String value = 'Tap to edit link';
  732. if (style.contains(NotusAttribute.link)) {
  733. value = style.value(NotusAttribute.link);
  734. }
  735. final clipboardEnabled = value != 'Tap to edit link';
  736. final body = !isEditing
  737. ? _LinkView(value: value, onTap: edit)
  738. : _LinkInput(
  739. key: _inputKey,
  740. controller: _inputController,
  741. formatError: _formatError,
  742. );
  743. final items = <Widget>[Expanded(child: body)];
  744. if (!isEditing) {
  745. final unlinkHandler = hasLink(style) ? unlink : null;
  746. final copyHandler = clipboardEnabled ? copyToClipboard : null;
  747. final openHandler = hasLink(style) ? openInBrowser : null;
  748. final buttons = <Widget>[
  749. toolbar.buildButton(context, ZefyrToolbarAction.unlink,
  750. onPressed: unlinkHandler),
  751. toolbar.buildButton(context, ZefyrToolbarAction.clipboardCopy,
  752. onPressed: copyHandler),
  753. toolbar.buildButton(
  754. context,
  755. ZefyrToolbarAction.openInBrowser,
  756. onPressed: openHandler,
  757. ),
  758. ];
  759. items.addAll(buttons);
  760. }
  761. final trailingPressed = isEditing ? doneEdit : closeOverlay;
  762. final trailingAction =
  763. isEditing ? ZefyrToolbarAction.confirm : ZefyrToolbarAction.close;
  764. return ZefyrToolbarScaffold(
  765. body: Row(children: items),
  766. trailing: toolbar.buildButton(
  767. context,
  768. trailingAction,
  769. onPressed: trailingPressed,
  770. ),
  771. );
  772. }
  773. }
  774. class _LinkInput extends StatefulWidget {
  775. final TextEditingController controller;
  776. final bool formatError;
  777. const _LinkInput(
  778. {Key key, @required this.controller, this.formatError = false})
  779. : super(key: key);
  780. @override
  781. _LinkInputState createState() {
  782. return _LinkInputState();
  783. }
  784. }
  785. class _LinkInputState extends State<_LinkInput> {
  786. final FocusNode _focusNode = FocusNode();
  787. ZefyrScope _editor;
  788. bool _didAutoFocus = false;
  789. @override
  790. void didChangeDependencies() {
  791. super.didChangeDependencies();
  792. if (!_didAutoFocus) {
  793. FocusScope.of(context).requestFocus(_focusNode);
  794. _didAutoFocus = true;
  795. }
  796. final toolbar = ZefyrToolbar.of(context);
  797. if (_editor != toolbar.editor) {
  798. _editor?.toolbarFocusNode = null;
  799. _editor = toolbar.editor;
  800. _editor.toolbarFocusNode = _focusNode;
  801. }
  802. }
  803. @override
  804. void dispose() {
  805. _editor?.toolbarFocusNode = null;
  806. _focusNode.dispose();
  807. _editor = null;
  808. super.dispose();
  809. }
  810. @override
  811. Widget build(BuildContext context) {
  812. final theme = Theme.of(context);
  813. final toolbarTheme = ZefyrTheme.of(context).toolbarTheme;
  814. final color =
  815. widget.formatError ? Colors.redAccent : toolbarTheme.iconColor;
  816. final style = theme.textTheme.subhead.copyWith(color: color);
  817. return TextField(
  818. style: style,
  819. keyboardType: TextInputType.url,
  820. focusNode: _focusNode,
  821. controller: widget.controller,
  822. autofocus: true,
  823. decoration: InputDecoration(
  824. hintText: 'https://',
  825. filled: true,
  826. fillColor: toolbarTheme.color,
  827. border: InputBorder.none,
  828. contentPadding: const EdgeInsets.all(10.0),
  829. ),
  830. );
  831. }
  832. }
  833. class _LinkView extends StatelessWidget {
  834. const _LinkView({Key key, @required this.value, this.onTap})
  835. : super(key: key);
  836. final String value;
  837. final VoidCallback onTap;
  838. @override
  839. Widget build(BuildContext context) {
  840. final theme = Theme.of(context);
  841. final toolbarTheme = ZefyrTheme.of(context).toolbarTheme;
  842. Widget widget = ClipRect(
  843. child: ListView(
  844. scrollDirection: Axis.horizontal,
  845. children: <Widget>[
  846. Container(
  847. alignment: AlignmentDirectional.centerStart,
  848. constraints: BoxConstraints(minHeight: ZefyrToolbar.kToolbarHeight),
  849. padding: const EdgeInsets.all(10.0),
  850. child: Text(
  851. value,
  852. maxLines: 1,
  853. overflow: TextOverflow.ellipsis,
  854. style: theme.textTheme.subhead
  855. .copyWith(color: toolbarTheme.disabledIconColor),
  856. ),
  857. )
  858. ],
  859. ),
  860. );
  861. if (onTap != null) {
  862. widget = GestureDetector(
  863. child: widget,
  864. onTap: onTap,
  865. );
  866. }
  867. return widget;
  868. }
  869. }