zefyr

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