diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 00000000..bde1342e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,26 @@ +--- +name: "\U0001F41B Bug Report" +about: Something isn't working as expected +--- + +## Bug Report + +**仅限中文与英文**, 其他语言的提交将直接被关闭 + +请先确认查找了已有的 issue [GitHub issues](https://github.com/apache/incubator-shardingsphere-example/issues). + +为了更好的收录您反馈或者提交的相关pr. 请您关注您提交的问题, 我们可能需要更多的详细信息, 我们会在issue下先您收集相关信息, +如果长时间未得到您的回复, 如果我们无法在某些环境上重现该问题, 并且您**超过7天未回复**, 我们可能会关 **闭掉issue**, 谢谢 + + +### 您当前的flutter doctor信息 + +### 预期的表现 + +### 实际的表现 + +### 预期的分析 (给出您能想到, 任何您能想到的) + +### 重现的方式, 例如从 A界面 点击 b, 跳转到B页面, 界面出现溢出乱码等. + +### 用于重现此问题或者可能解决以上问题的示例代码(例如github 链接代码) diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 00000000..478e88df --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,18 @@ +--- +name: "\U0001F680 Feature Request" +about: I have a suggestion +--- + +## Feature Request + +**仅限中文与英文**, 其他语言的提交将直接被关闭 + +请先确认查找了已有的 issue [GitHub issues](https://github.com/apache/incubator-shardingsphere-example/issues). + +为了更好的收录您反馈或者提交的相关pr. 请您关注您提交的问题, 我们可能需要更多的详细信息, 我们会在issue下先您收集相关信息, +如果长时间未得到您的回复, 如果我们无法在某些环境上重现该问题, 并且您**超过7天未回复**, 我们可能会关 **闭掉issue**, 谢谢 + + +### 您的功能需求是否与哪些问题有关? + +### 描述您想要的功能. diff --git a/.github/ISSUE_TEMPLATE/page-about.md b/.github/ISSUE_TEMPLATE/page-about.md new file mode 100644 index 00000000..d79ca10a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/page-about.md @@ -0,0 +1,62 @@ +--- +name: "📄 Page About" +about: something about page +--- + +## Page About + +**仅限中文与英文**, 其他语言的提交将直接被关闭 + +请先确认查找了已有的 issue [GitHub issues](https://github.com/apache/incubator-shardingsphere-example/issues). + +为了更好的收录您反馈或者提交的相关pr. 请您关注您提交的问题, 我们可能需要更多的详细信息, 我们会在issue下先您收集相关信息, +如果长时间未得到您的回复, 如果我们无法在某些环境上重现该问题, 并且您**超过7天未回复**, 我们可能会关 **闭掉issue**, 谢谢 + + + + +## 界面增加或者更新的内容概括 + +## 界面数据 + +例如: +``` +{ + "name": "standard_for_slider", + "screenShot": "", + "author":"sanfan", + "title":"slider组件", + "email": "hanxu@qq.com", + "desc": "slider, new Slider", + "id": "8ab2b5c2_42ae_4241_9c8a_5c9e1f92b096" +} + +``` +## Page 关联的 DEMO 信息 + +例如: + +``` +{ + "name": "intor page", + "screenShot": "", + "author":"sanfan", + "title":"介绍页", + "email": "hanxu317@qq.com", + "desc": "desc", + "id": "ee4feb8e_32ae_4241_9c8a_5c9e1f92b096" +}, +{ + "name": "intor pag2e", + "screenShot": "", + "author":"sanfan", + "title":"介绍页", + "email": "hanxu317@qq.com", + "desc": "desc", + "id": "ee4feb8e_32ae_4241_9c8a_5c9e1f92b097" +} +``` + +## 引入第三方包的文件与版本号(如果有引入, 请标明) + + diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 00000000..0a142371 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,14 @@ +--- +name: "\U0001F914 Question" +about: Usage question that isn't answered in docs or discussion +--- + +## Question + +**仅限中文与英文**, 其他语言的提交将直接被关闭 + +请先确认查找了已有的 issue [GitHub issues](https://github.com/apache/incubator-shardingsphere-example/issues). + +为了更好的收录您反馈或者提交的相关pr. 请您关注您提交的问题, 我们可能需要更多的详细信息, 我们会在issue下先您收集相关信息, +如果长时间未得到您的回复, 如果我们无法在某些环境上重现该问题, 并且您**超过7天未回复**, 我们可能会关 **闭掉issue**, 谢谢 + diff --git a/.github/issue_template.md b/.github/issue_template.md deleted file mode 100644 index e69de29b..00000000 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e69de29b..aca9f499 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -0,0 +1,19 @@ +#### What does this PR do? + +[TrelloCard/Issue/Story](LINK_TO_STORY) + +##### Why are we doing this? Any context or related work? + +#### Where should a reviewer start? + +#### Manual testing steps? + +#### Screenshots + +--- + +#### Database changes + +#### Deployment instructions + +#### New ENV variables diff --git a/.gitignore b/.gitignore index 4f66b1c5..bdc2c7bc 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ .metadata + # IntelliJ related *.ipr @@ -33,9 +34,14 @@ .flutter-plugins .packages .pub-cache/ +pubspec.lock .pub/ /build/ +# goCli related +go-cli/.packages +go-cli/pubspec.lock + # Android related **/android/**/gradle-wrapper.jar **/android/.gradle diff --git a/CHANGE-LOG.md b/CHANGE-LOG.md index a625fbfe..88b73e46 100644 --- a/CHANGE-LOG.md +++ b/CHANGE-LOG.md @@ -1,86 +1,358 @@ ## 更新日志 - +#### 8/28/2019 + - [x] Modify: IsOpen 的移步判断 + - [x] Update index.md + - [x] Update index.dart +#### 5/28/2019 + - [x] change:async master + - [x] modfiy:应用商店 + - [x] Merge pull request #222 from DeckeDeng/develop + - [x] add home scaffold +#### 8/22/2019 + - [x] Merge pull request #322 from alibaba/beta + - [x] Modify: web view 方法回退 + - [x] Merge pull request #321 from alibaba/beta + - [x] Merge pull request #317 from alibaba/beta + - [x] Merge branch 'master' into beta + - [x] Update contribute.md +#### 8/12/2019 + - [x] update: doc & tpl + - [x] Update tpl.md + - [x] Merge pull request #313 from Nealyang/beta + - [x] 添加登陆错误提醒 +#### 3/11/2019 + - [x] Update README-en.md +#### 8/15/2019 + - [x] update: router path + - [x] ci + - [x] update: view remote code + - [x] 整理文件 +#### 8/13/2019 + - [x] Modify + - [x] Modfiy: + - [x] update: page issue template +#### 8/1/2019 + - [x] merge code +#### 7/29/2019 + - [x] Merge branch 'master' of github.com:alibaba/flutter-go into beta + - [x] delete .github no use file + - [x] docs(update: pr template): + - [x] del: no use file + - [x] Merge branch 'master' of github.com:alibaba/flutter-go + - [x] docs(add widget about issue): + - [x] Merge branch 'temp' into beta + - [x] 迁移位置 + - [x] update: template + - [x] update: 处理合并导致的总理2 +#### 8/7/2019 + - [x] fix bug + - [x] Merge pull request #302 from Nealyang/beta + - [x] remove get collection + - [x] tep + - [x] update: 完成markdown动态更新 +#### 8/8/2019 + - [x] remove user appBar + - [x] bottomBar 添加个人中心 + - [x] Merge pull request #309 from DeckeDeng/beta2 + - [x] ios project.pbxproj + - [x] Merge pull request #308 from DeckeDeng/beta2 + - [x] Merge pull request #304 from Nealyang/beta + - [x] 修改 ios 配置文件 + - [x] update: 临时增加code-review页 +#### 6/28/2019 + - [x] Merge pull request #264 from DeckeDeng/beta2 + - [x] Merge pull request #263 from DeckeDeng/beta2 + - [x] Merge branch 'beta' into beta2 + - [x] 消息反馈页面 +#### 2/1/2019 + - [x] Merge branch 'develop' of https://github.com/alibaba/flutter-go into develop + - [x] feat:android splash + - [x] feat:add android splash + - [x] Merge pull request #116 from hanxu317317/develop + - [x] docs(更新Readme中环境信息): + - [x] Merge pull request #115 from hanxu317317/develop + - [x] feat(首页欢迎图, 加入第一次访问判断.): + - [x] Merge branch 'develop' into sptil + - [x] update: ios启动图描述 + - [x] fix:yaml + - [x] fix: + - [x] Merge pull request #113 from hanxu317317/develop + - [x] fix:mtl buidl apk test + - [x] refactor(conflict): + - [x] add sp + - [x] fix(解决由于flutter 版本问题导致的报错): + - [x] fix:back + - [x] fix:package err + - [x] fix:modify github package name fail and test + - [x] fix:build before modify package's name +#### 7/12/2019 + - [x] Merge pull request #279 from Nealyang/beta + - [x] add + - [x] d +#### 8/6/2019 + - [x] issuse message + - [x] Merge pull request #294 from Nealyang/beta + - [x] 修改为线上地址 + - [x] 个人中心、收藏、搜索 +#### 7/25/2019 + - [x] Merge branch 'beta' of github.com:alibaba/flutter-go +#### 7/23/2019 + - [x] zefyr +#### 7/22/2019 + - [x] feature:个人设置 +#### 7/15/2019 + - [x] appstore 反馈错误 + - [x] readme错误 + - [x] 标题错误,跳转错误 +#### 7/11/2019 + - [x] update: 迁移新老数据结构与收藏 + - [x] update: 增加环境配置. 挂载在application静态属性上 + - [x] update: 修改文章 +#### 4/8/2019 + - [x] flutter project switch ios or android by androidstudio + - [x] android permission + - [x] remove extra files +#### 6/18/2019 + - [x] 推送,ios/android,test +#### 6/15/2019 + - [x] 删除node相关 + - [x] Merge branch 'develop' of github.com:alibaba/flutter-common-widgets-app into web + - [x] update: 完善demo + - [x] update: 界面增加title属性 + - [x] add: cli使用说明 +#### 6/11/2019 + - [x] update: 替换markdown包引入方式 + - [x] add: 加入标准page模板 + - [x] 加入标准page + - [x] 加入初始化demo + - [x] 引入三方flutter_markdown gi +#### 6/10/2019 + - [x] Merge pull request #249 from Nealyang/beta + - [x] 添加收藏功能 + - [x] update: 标准化代码. + - [x] goCli 完成90% +#### 6/4/2019 + - [x] markdown + - [x] 加入初始gocli + - [x] add page demo + - [x] 增加page的统一入口 + - [x] add demos packges + - [x] Merge pull request #240 from Nealyang/beta + - [x] 退出登陆、feedback 测试 +#### 5/31/2019 + - [x] 添加个人中心 +#### 5/30/2019 + - [x] 首页不刷新 + - [x] Merge pull request #1 from forever-713/zuston-patch-1 + - [x] Fix App name error +#### 5/29/2019 + - [x] 添加 github oAuth 认证,添加错误提醒 +#### 5/23/2019 + - [x] readme.md + - [x] Merge pull request #219 from weikx/develop + - [x] Merge pull request #215 from DeckeDeng/develop + - [x] delete firstPage + - [x] de + - [x] Test:测试分支 + - [x] Merge branch 'develop' of https://github.com/weikx/flutter-go into develop + - [x] Change word 'Free' into 'Three' + - [x] Chang word 'Free' into 'Three' +#### 5/17/2019 + - [x] Merge pull request #216 from alibaba/master +#### 5/14/2019 + - [x] update version +#### 2/3/2019 + - [x] fix(解决ios报错): + - [x] Merge pull request #119 from Nealyang/master + - [x] fix:fix code conflic + - [x] fix: code conflict + - [x] Merge branch 'develop' into master + - [x] fix:view code + - [x] Merge pull request #118 from alibaba/dev/yisheng +#### 5/9/2019 + - [x] test version +#### 5/7/2019 + - [x] merge origin + - [x] add update test + - [x] Merge branch 'develop' of github.com:alibaba/flutter-go + - [x] cookie 校验 + - [x] modify home.dart bottom tab + - [x] session 验证 + - [x] merge develop + - [x] 登陆 + - [x] Merge pull request #205 from alibaba/dev/sanl + - [x] modify bottom tab + - [x] delete ios file + - [x] add apk + - [x] Login 登陆界面 +#### 5/6/2019 + - [x] add login +#### 5/1/2019 + - [x] Modfiy: 改造业内资讯页面 +#### 4/30/2019 + - [x] Add:二期开发-重构首页布局 + - [x] gradle 4.10.2 包添加AndroidX配置 +#### 2/19/2019 + - [x] packagename + - [x] Merge pull request #148 from alibaba/dev/yisheng + - [x] refactor:按照代码规范调整import 文件 + - [x] update: 规范 + - [x] Merge pull request #147 from Nealyang/master + - [x] fix:修复TabBar demo + - [x] Merge pull request #146 from Nealyang/master + - [x] refactor: 根据规范重构代码 + - [x] Merge pull request #145 from alibaba/dev/yisheng + - [x] refactor:按照代码规范调整markdow解析 + - [x] refactor(规范化代码): + - [x] Merge pull request #143 from Nealyang/master + - [x] refactor:按规范修改代码、注释等 + - [x] Merge pull request #142 from Nealyang/master + - [x] commit + - [x] Merge pull request #141 from Nealyang/master + - [x] Merge pull request #140 from Nealyang/master + - [x] Merge pull request #135 from hanxu317317/develop + - [x] Merge branch 'develop' into develop + - [x] Merge pull request #139 from Nealyang/master + - [x] Merge branch 'develop' of github.com:alibaba/flutter-common-widgets-app + - [x] refactor:根据规范,重构代码 + - [x] modify code comments + - [x] Merge pull request #138 from alibaba/dev/yisheng + - [x] refactor:按照代码规范调整注释文件 +#### 4/29/2019 + - [x] test app store + - [x] Merge branch 'master' into dev/sanl + - [x] 修改bug +#### 4/26/2019 + - [x] Debug: GoogleService-Info.plist 位置问题 导致错误 + - [x] change LICENSE date + - [x] 变更许可 + - [x] Merge pull request #189 from alibaba/develop + - [x] change License +#### 4/22/2019 + - [x] release apk + - [x] Merge pull request #192 from alibaba/dev/yisheng + - [x] 格式化 + - [x] Merge pull request #191 from alibaba/dev/yisheng + - [x] Delete:删除 .gradle 文件夹 + - [x] Merge pull request #188 from alibaba/dev/yisheng +#### 4/17/2019 + - [x] gridView 网格效果 图片流 + - [x] 添加渐变效果/网络图片覆盖图层渲染/图片填充 +#### 1/31/2019 + - [x] Merge pull request #112 from alibaba/dev/yisheng + - [x] Debug:canvas 路由问题 + - [x] Merge pull request #111 from alibaba/dev/yisheng + - [x] feat: Canvas 细化各种方法 + - [x] add file + - [x] fix:code + - [x] feat(加入启动图, 时间2秒): +#### 4/12/2019 + - [x] update: 修改错字 + - [x] Merge pull request #180 from alibaba/develop + - [x] add: doc roadmap +#### 4/1/2019 + - [x] remove recruit + - [x] move recruit +#### 3/31/2019 + - [x] 增加模板 + - [x] Create pull_request_template.md + - [x] Delete .github + - [x] Create .github + - [x] Merge pull request #173 from alibaba/master +#### 2/2/2019 + - [x] fix: 部分代码添加 mounted + - [x] Merge pull request #117 from alibaba/dev/yisheng + - [x] fix:修复Canvas组件收藏的bug +#### 2/20/2019 + - [x] 同步文件 + - [x] Merge pull request #152 from hanxu317317/develop + - [x] 更新跳转页方式 + - [x] modify logo + - [x] Merge pull request #149 from hanxu317317/develop + - [x] fix(解决返回首页报错的问题): +#### 2/11/2019 + - [x] fix: 部分代码 analysis 解决 #### 2019-2-5 - - [x] 处理因为flutter版本导致的项目运行不起来 - - [x] 更新readme, 加入开发日志, 与相关说明 - - [x] 加入 首页欢迎效果图 - - [x] refactor(整理richText的说明): - - [x] 解决一些页面的code演示打不开的问题 - - [x] add:开发规范 - - [x] add:版本更新历史链接 - - [x] Update README.md - - [x] add:添加版本号 - - [x] feat:添加代码开发规范 - - [x] refactor(update: version & fiexed warns): - - [x] fix(solve conflict): - - [x] modify:toast and andrid apk label - - [x] Add:自动 pr 工具抓取器,抓取两周前至今的,提交数据,并去重 - - [x] fix:fluttetToast backHome - - [x] fix:modified the style of toast && remote files - - [x] chore(删除tools/log.json): - - [x] 重构文件结构 - - [x] 关于手册图标更换 - - [x] 增加demo: CupertinoNavigationBar CupertinoPageRoute CupertinoPageScaffold CupertinoPicker,CupertinoPopupSurface CupertinoTimerPickerDemo +- [x] 处理因为flutter版本导致的项目运行不起来 +- [x] 更新readme, 加入开发日志, 与相关说明 +- [x] 加入 首页欢迎效果图 +- [x] refactor(整理richText的说明): +- [x] 解决一些页面的code演示打不开的问题 +- [x] add:开发规范 +- [x] add:版本更新历史链接 +- [x] Update README.md +- [x] add:添加版本号 +- [x] feat:添加代码开发规范 +- [x] refactor(update: version & fiexed warns): +- [x] fix(solve conflict): +- [x] modify:toast and andrid apk label +- [x] Add:自动 pr 工具抓取器,抓取两周前至今的,提交数据,并去重 +- [x] fix:fluttetToast backHome +- [x] fix:modified the style of toast && remote files +- [x] chore(删除tools/log.json): +- [x] 重构文件结构 +- [x] 关于手册图标更换 +- [x] 增加demo: CupertinoNavigationBar CupertinoPageRoute CupertinoPageScaffold CupertinoPicker,CupertinoPopupSurface CupertinoTimerPickerDemo #### 2019-1-24 - - [x] 功能:更新小部件的图标 - - [x] 功能:添加CupertinoTimerPickerDemo - - [x] 调试:消除警告 - - [x] 修复:关于手册图标更换 - - [x] 添加:文案描述 - - [x] 添加:CupertinoPicker,CupertinoPopupSurface +- [x] 功能:更新小部件的图标 +- [x] 功能:添加CupertinoTimerPickerDemo +- [x] 调试:消除警告 +- [x] 修复:关于手册图标更换 +- [x] 添加:文案描述 +- [x] 添加:CupertinoPicker,CupertinoPopupSurface #### 2019-1-23 - - [x] 修复: 导航栏home返回报错 - - [x] 修复:收集错误 - - [x] 添加:CupertinoNavigationBar CupertinoPageRoute CupertinoPageScaffold +- [x] 修复: 导航栏home返回报错 +- [x] 修复:收集错误 +- [x] 添加:CupertinoNavigationBar CupertinoPageRoute CupertinoPageScaffold #### 2019-1-22 - - [x] 功能:在Allsimon拉请求中添加英文简介 +- [x] 功能:在Allsimon拉请求中添加英文简介 #### 2019-1-21 - - [x] 功能:Cupertino的子项 +- [x] 功能:Cupertino的子项 #### 2019-1-20 - - [x] 功能:CupertinoSwitch演示 - - [x] 功能:为搜索列表加入图标 - - [x] 功能:CupertinoSliverRefreshControl演示 - - [x] 功能:CupertinoSliverNavigationBar演示 +- [x] 功能:CupertinoSwitch演示 +- [x] 功能:为搜索列表加入图标 +- [x] 功能:CupertinoSliverRefreshControl演示 +- [x] 功能:CupertinoSliverNavigationBar演示 #### 2019-1-18 - - [x] 更新:SharedPreferences保存数据和android设备布局溢出 - - [x] 功能:添加CupertinoScrollbar演示 - - [x] 功能:第四页暂时用欢迎页替代。后期再开发 +- [x] 更新:SharedPreferences保存数据和android设备布局溢出 +- [x] 功能:添加CupertinoScrollbar演示 +- [x] 功能:第四页暂时用欢迎页替代。后期再开发 #### 2019-1-17 - - [x] 添加:+许可证 +- [x] 添加:+许可证 #### 2019-1-16 - - [x] 转换:将README翻译为En语言环境 - - [x] 功能:CupertinoScrollbar演示 +- [x] 转换:将README翻译为En语言环境 +- [x] 功能:CupertinoScrollbar演示 #### 2019-1-14 - - [x] 添加:增加手册页面 - - [x] 功能:文字演示 - - [x] 重构:修改过的图标 - - [x] 重构:文档,文章,组件收藏,新增webView - - [x] 重构:修改过的演示 - - [x] 重构:代码视图 - - [x] 更新:版本 和readme.md - - [x] 修改:添加代码视图 - - [x] 功能:添加搜索历史记录板 - - [x] 修改:列出加标头错误 +- [x] 添加:增加手册页面 +- [x] 功能:文字演示 +- [x] 重构:修改过的图标 +- [x] 重构:文档,文章,组件收藏,新增webView +- [x] 重构:修改过的演示 +- [x] 重构:代码视图 +- [x] 更新:版本 和readme.md +- [x] 修改:添加代码视图 +- [x] 功能:添加搜索历史记录板 +- [x] 修改:列出加标头错误 #### 2019-1-15 - - [x] 功能:welcomepage +- [x] 功能:welcomepage #### 2019-1-13 - - [x] 添加:一些输入描述 - - [x] 功能:加入GridPaper&SliverGrid - - [x] 重构:修改db - - [x] 重构:删除数据库 TabBarView - - [x] 添加:网格视图 - - [x] 修改:checkbosListTile 错误 - - [x] 修改:自动提示文案 - - [x] 功能:增加免责声明,声明组件,自动弹出,左上角入口 - - [x] 重构:整理数据库初始逻辑,判断数据库完整性,判断是否存在已知的cat,widget,collection 三张表。 - - [x] 修复:DialogDemo,无法关闭的问题 +- [x] 添加:一些输入描述 +- [x] 功能:加入GridPaper&SliverGrid +- [x] 重构:修改db +- [x] 重构:删除数据库 TabBarView +- [x] 添加:网格视图 +- [x] 修改:checkbosListTile 错误 +- [x] 修改:自动提示文案 +- [x] 功能:增加免责声明,声明组件,自动弹出,左上角入口 +- [x] 重构:整理数据库初始逻辑,判断数据库完整性,判断是否存在已知的cat,widget,collection 三张表。 +- [x] 修复:DialogDemo,无法关闭的问题 #### 2019-1-12 - - [x] 修复:icon没有,但内容有的,组件,给补充了icon - - [x] 修改:1.整理文件 2.修正分析 - - [x] 更新:flutter_rookie_book => flutter_go - - [x] 更新:更新SearchInput文件名=> search_input - - [x] 修改:文件名称的大小写规范 - - [x] 修改:修正bottomNavigationBar iconButton警告 +- [x] 修复:icon没有,但内容有的,组件,给补充了icon +- [x] 修改:1.整理文件 2.修正分析 +- [x] 更新:flutter_rookie_book => flutter_go +- [x] 更新:更新SearchInput文件名=> search_input +- [x] 修改:文件名称的大小写规范 +- [x] 修改:修正bottomNavigationBar iconButton警告 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..905b4fab --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at zhu.yan@alibaba-inc.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..fc13245c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1 @@ +[Flutter Go 共建](https://github.com/alibaba/flutter-go/blob/master/docs/contribute.md) diff --git a/FlutterGo.apk b/FlutterGo.apk deleted file mode 100644 index 01ce3048..00000000 Binary files a/FlutterGo.apk and /dev/null differ diff --git a/README-en.md b/README-en.md index 4283a860..9eeeec99 100644 --- a/README-en.md +++ b/README-en.md @@ -1,28 +1,105 @@ -## Flutter Go +# Flutter Go +[中文简体](https://github.com/alibaba/flutter-go/blob/master/README.md) | Language: [English](https://github.com/alibaba/flutter-go/blob/master/README-en.md) ![https://img.alicdn.com/tfs/TB1OJkeHNYaK1RjSZFnXXa80pXa-229-229.png](https://img.alicdn.com/tfs/TB1OJkeHNYaK1RjSZFnXXa80pXa-229-229.png) -> Help developers get started quickly Flutter **Flutter Go 1.0 Android has been released** +> Help developers get started quickly Flutter +## News +### 🔥 `Flutter Go 2.0` released +> Flutter Go 2.0 according to [The Flutter-Go Roadmap for 2019](https://github.com/alibaba/flutter-go#the-flutter-go-roadmap%E8%B7%AF%E7%BA%BF%E5%9B%BE-for-2019) beginning of the year, the new feature is: -## Download URL +
mark:✔ is this version
-Android download URL: +- [x] [ `Flutter Go` website ](https://flutter-go.pub/website/) (news,publish,learn) +- [x] [ `Flutter Go web` ](https://github.com/alibaba/flutter-go/tree/web/flutter-go-web-0.0.1) (web version to learn) + - [online preview ](https://flutter-go.pub/flutter_go_web/) + - [resource ](https://github.com/alibaba/flutter-go/tree/web/flutter-go-web-0.0.1) +<<<<<<< HEAD <<<<<<< HEAD ======= >>>>>>> develop +======= +- [x] [ `Flutter Go Widget` `pull request` rules ( the third )](https://github.com/alibaba/flutter-go/blob/master/docs/contribute.md) +>>>>>>> dxj/master -Iphone download URL: -No +- [x] user center (yourself `widget`) + - login (with `GitHub`) + - search (search about `Flutter`) + - favorite component (save online) + - issues (APP online `ISSUES`) + - favorite (APP favorite) +- [x] [ `go-cli` tool ](https://github.com/alibaba/flutter-go/blob/master/docs/widget.md) + - `Flutter Go` `pull request` tools + - [ build `Flutter Go Widget` standard template with terminal ](https://github.com/alibaba/flutter-go/blob/beta/docs/widget.md) + - `markdown` template (when merge master breach) +- [x] ` Flutter Go ` `APP` auto upgrade +- [ ] template sync ( pc, native ) +- [ ] `Flutter Go` store + +## Third part build + +Because the content of ** flutter ** is updated faster, we can't enrich the content of the project faster, If you are willing to contribute to the development and learning of flutter, please see the [build instructions](https://github.com/alibaba/flutter-go/blob/master/docs/contribute.md) before you start, we will put the builder's avatar and nick to the websit. + +## Logs + +[resource](https://github.com/alibaba/flutter-go/blob/develop/CHANGE-LOG.md) + +## Development specification +> we are based on the official [dart language specification](https://www.dartlang.org)。 + +[<< Flutter Go development specification >>](https://github.com/alibaba/flutter-go/blob/develop/Flutter_Go%20%E4%BB%A3%E7%A0%81%E5%BC%80%E5%8F%91%E8%A7%84%E8%8C%83.md) + +## The Flutter-Go Roadmap for 2019 +> Considering Flutter's future changes and strategy variability, roadmap does not rule out some adjustments, but overall it will not change much. + + + +## How to use + +- checkout version +```dart + flutter --version +``` +- run doctor +```dart + flutter doctor +``` +- start app +```dart + flutter packages get + flutter run +``` + +- other issues + - https://flutterchina.club/setup-macos/ + - https://flutter.dev/docs/get-started/install/macos + +## Release + +### Android: + +- [Download](https://github.com/alibaba/flutter-go/blob/master/android/app/release/app-release.apk) + +### Ios: + +- AppStore search "Fluttergo" ## Development Environment This Project need latest package, please update regularly. -- dart(version: 2.0.0) -- flutter(version: v1.0.0) +```dart +flutter --version +dart --version +pub --version + +// Flutter (Channel beta, v1.7.8) +// Dart VM version: 2.4.0 +// Pub 2.4.0 +``` ### Background @@ -89,4 +166,4 @@ The advantages of Flutter mainly include: -Powered by Alibaba Auction Front-end Team +Powered by Alibaba Auction Front-end Team \ No newline at end of file diff --git a/README.md b/README.md index 3b5cc4fa..75aeae66 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,44 @@ +# Flutter Go Language: [English](https://github.com/alibaba/flutter-go/blob/master/README-en.md) | [中文简体](https://github.com/alibaba/flutter-go/blob/master/README.md) -## Flutter Go ![https://img.alicdn.com/tfs/TB1OJkeHNYaK1RjSZFnXXa80pXa-229-229.png](https://img.alicdn.com/tfs/TB1OJkeHNYaK1RjSZFnXXa80pXa-229-229.png) -> 帮助开发者快速上手 Flutter **Flutter Go 1.0 Android版已正式发布** +> 一个帮助开发者快速上手的 `APP` + +## 最新动态 +### 🔥 `Flutter Go 2.0` 发布 +> Flutter Go 2.0 秉承年初发布的 [The Flutter-Go Roadmap(路线图) for 2019](https://github.com/alibaba/flutter-go#the-flutter-go-roadmap%E8%B7%AF%E7%BA%BF%E5%9B%BE-for-2019) 已经做了新版本的迭代,新版本如下新功能: + +
注:✔是本次发布内容
+ +- [x] [ `Flutter Go` 官方网站 ](https://flutter-go.pub/website/) (官方消息,发布,学习) +- [x] [ `Flutter Go web` 版本](https://github.com/alibaba/flutter-go/tree/web/flutter-go-web-0.0.1) (web 版本学习帮助) + - [线上预览版 ](https://flutter-go.pub/flutter_go_web/) + - [项目地址 ](https://github.com/alibaba/flutter-go/tree/web/flutter-go-web-0.0.1) + +- [x] [ `Flutter Go Widget` 的 `pull request` 提交规范(第三方共建)](https://github.com/alibaba/flutter-go/blob/master/docs/contribute.md) + +- [x] 用户中心 (专属个人的`widget`案例) + - 用户登录(通过`GitHub`账户) + - 全网搜索 (全网搜索 `Flutter` 资讯) + - 收藏个人组件(保存到远端) + - 反馈建议 (APP 在线 `ISSUES`) + - 分享链接 (APP分享) +- [x] [ `go-cli` 工具 ](https://github.com/alibaba/flutter-go/blob/master/docs/widget.md) + - `Flutter Go` 的 `pull request` 工具 + - [命令行 生成 `Flutter Go Widget` 标准公共模版](https://github.com/alibaba/flutter-go/blob/beta/docs/widget.md) + - `markdown` 模版动态化生成(合并到master分支后) +- [x] ` Flutter Go ` 官方 `APP` 版本自动升级 +- [ ] 多端模版同步( pc端,native端同步 ) +- [ ] `Flutter Go` store + + + +## 三方共建说明 + +由于 **flutter** 内容更新较快. 我们无法更快的丰富项目的内容. 如果您愿意为国内flutter的发展与学习贡献自己的力量, 请参考我们的 [共建说明](https://github.com/alibaba/flutter-go/blob/master/docs/contribute.md), 我们会将共建者的头像姓名贡献至我们的官网. + + ## 版本更新历史 > 按时间顺序,展示重要的提交更新内容。 @@ -16,13 +51,41 @@ Language: [English](https://github.com/alibaba/flutter-go/blob/master/README-en. [<< Flutter Go 开发规范第一版 >>](https://github.com/alibaba/flutter-go/blob/develop/Flutter_Go%20%E4%BB%A3%E7%A0%81%E5%BC%80%E5%8F%91%E8%A7%84%E8%8C%83.md) <<<<<<< HEAD +<<<<<<< HEAD +======= +## The Flutter-Go Roadmap(路线图) for 2019 +> 考虑到 Flutter 未来的变化和策略的可变性, roadmap 不排除有一定调整,但总体不会变化太大。 + + + +## 运行方式 + +- 查看一下版本号是否正确 +```dart + flutter --version +``` +- 运行以下命令查看是否需要安装其它依赖项来完成安装 +```dart + flutter doctor +``` +- 运行启动您的应用 +```dart + flutter packages get + flutter run +``` + +- 如果有其他问题,请参考 + - https://flutterchina.club/setup-macos/ + - https://flutter.dev/docs/get-started/install/macos +>>>>>>> dxj/master ## Release安装包下载地址 ### android正式版,下载地址: - 华为市场已上线,华为应用市场搜索 "Fluttergo"或者直接[点击下载](https://appstore.huawei.com/search/fluttergo) - +- [直接 apk 文件下载](https://github.com/alibaba/flutter-go/blob/master/android/app/release/app-release.apk) + ### iphone正式版,下载地址: @@ -55,8 +118,17 @@ iphone下载地址: AppStore上面进行搜索fluttego ## 基础环境 本项目环境持续更新. 请定期更新各依赖包. -- dart(version: 2.0.0) -- flutter(version: v1.0.0) +```dart +// 运行如下命令 +flutter --version +dart --version +pub --version + +// 正确环境如下 +// Flutter (Channel beta, v1.7.8) +// Dart VM version: 2.4.0 +// Pub 2.4.0 +``` ### 背景 diff --git a/android/app/build.gradle b/android/app/build.gradle index b723070c..4d293326 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -59,8 +59,23 @@ android { versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + manifestPlaceholders = [ + JPUSH_PKGNAME : "com.alibaba.fluttergo", + JPUSH_APPKEY : "62eb07d227d1f11dd7fa6239", //JPush上注册的包名对应的appkey. + JPUSH_CHANNEL : "developer-default", + ] + +// ndk { +// //选择要添加的对应cpu类型的.so库。 +// abiFilters 'armeabi', 'armeabi-v7a','x86', 'x86_64', 'mips'//, 'arm64-v8a' +// // 还可以添加 'x86', 'x86_64', 'mips', 'mips64' +// } + } + + signingConfigs { release { keyAlias keystoreProperties['keyAlias'] @@ -94,6 +109,8 @@ dependencies { ///implementation 'com.google.firebase:firebase-perf:16.2.3' // 登陆 ////implementation 'com.google.firebase:firebase-auth:16.0.3' + + } //firebase apply plugin: 'com.google.gms.google-services' diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c50aa871..30748444 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,21 +7,22 @@ additional functionality it is fine to subclass or reimplement FlutterApplication and put your custom class here. --> - - - - + + + + + android:icon="@mipmap/ic_launcher_logo" + android:usesCleartextTraffic="true"> @@ -14,9 +18,9 @@ - + - + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index d94b59b8..c6bb52b8 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -22,6 +22,8 @@ ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) + LSApplicationCategoryType + LSRequiresIPhoneOS NSCameraUsageDescription diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements new file mode 100644 index 00000000..903def2a --- /dev/null +++ b/ios/Runner/Runner.entitlements @@ -0,0 +1,8 @@ + + + + + aps-environment + development + + diff --git a/lib/api/api.dart b/lib/api/api.dart index 4ed411f6..748e3422 100644 --- a/lib/api/api.dart +++ b/lib/api/api.dart @@ -1,6 +1,6 @@ class Api{ - // static const String BASE_URL = 'http://127.0.0.1:6001/'; - static const String BASE_URL = 'http://flutter-go.alibaba.net/'; +// static const String BASE_URL = 'http://flutter-go.alibaba.net/'; + static const String BASE_URL = 'https://flutter-go.pub/api/'; static const String DO_LOGIN = BASE_URL+'doLogin';//登陆 @@ -8,5 +8,29 @@ class Api{ static const String LOGOUT = BASE_URL+'logout';//退出登陆 + static const String GET_USER_INFO = BASE_URL+'getUserInfo';//获取用户信息 + + static const String VERSION = BASE_URL+'getAppVersion';//检查版本 -} \ No newline at end of file + + static const String FEEDBACK = BASE_URL+'auth/feedback';//建议反馈 + +// static const String LOTOUT = BASE_URL+'logout';//退出登陆 + + static const String GET_ALL_COLLECTION = BASE_URL+'auth/getAllUserCollection';//获取全部收藏 + + static const String REMOVE_COLLECTION = BASE_URL+'auth/removeCollection';//移除收藏 + + static const String ADD_COLLECTION = BASE_URL+'auth/addCollection';//添加收藏 + + static const String CHECK_COLLECTED = BASE_URL+'checkCollected';//校验收藏 + + static const String SET_THEMECOLOR = BASE_URL+'auth/setThemeColor';//设置主题颜色 + + static const String GET_THEMECOLOR = BASE_URL +'/getThemeColor';//获取主题颜色 + + static const String GET_WIDGET_TREE = BASE_URL + 'getCateList';//获取widget列表树 + + static const String SEARCH_WIDGET = BASE_URL+'searchWidget';//搜索组件 +} + diff --git a/lib/blocs/bak/search_api.dart b/lib/blocs/bak/search_api.dart deleted file mode 100644 index 0548d34e..00000000 --- a/lib/blocs/bak/search_api.dart +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Created with Android Studio. - * User: 一晟 - * Date: 2019/4/28 - * Time: 3:20 PM - * email: zhu.yan@alibaba-inc.com - * tartget: FlatButton 的示例 - */ -import 'dart:async'; -import 'package:dio/dio.dart'; -import 'dart:convert'; -import './search_result.dart'; -import 'package:html/parser.dart' show parse; - -var dio = new Dio(); -class Api { - Future> search(name) async { - print('=========>>>'); - var response = await dio.get("https://www.so.com/s?ie=utf-8&q=$name"); -// var document = parse(response.data); -// var app = document.querySelectorAll('.res-title a'); - List res = []; -// app.forEach((f) { -// res.add( -// SearchResult( -// title: f.text, -// source: f.attributes["data-url"] ?? f.attributes["href"], -// ), -// ); -// }); - return res; - } -} - -Api api = Api(); \ No newline at end of file diff --git a/lib/blocs/bak/search_bloc.dart b/lib/blocs/bak/search_bloc.dart deleted file mode 100644 index 08d526c8..00000000 --- a/lib/blocs/bak/search_bloc.dart +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Created with Android Studio. - * User: 一晟 - * Date: 2019/4/28 - * Time: 7:17 PM - * email: zhu.yan@alibaba-inc.com - * tartget: - */ -import 'dart:async'; -import 'package:bloc/bloc.dart'; - -import './search_event.dart'; -import './search_state.dart'; -import './search_api.dart'; - - -/// 这里导入api类与上面的SearchEvent与SearchState文件 - -class SearchBloc extends Bloc { - @override - SearchState get initialState => SearchUninitialized(); - - @override - Stream mapEventToState(SearchEvent event,) async* { - if (event is SearchFetch) { - try { - yield SearchLoading(); - final res = await api.search(event.query); - yield SearchLoaded(res: res); - } catch (_) { - yield SearchError(); - } - } - } -} \ No newline at end of file diff --git a/lib/blocs/bak/search_event.dart b/lib/blocs/bak/search_event.dart deleted file mode 100644 index 0ecc0b44..00000000 --- a/lib/blocs/bak/search_event.dart +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Created with Android Studio. - * User: 一晟 - * Date: 2019/4/28 - * Time: 7:18 PM - * email: zhu.yan@alibaba-inc.com - * tartget: - */ -abstract class SearchEvent {} - -class SearchFetch extends SearchEvent { - final String query; - - SearchFetch({this.query}); - - @override - String toString() => 'SearchFetch:获取搜索结果事件'; -} \ No newline at end of file diff --git a/lib/blocs/bak/search_result.dart b/lib/blocs/bak/search_result.dart deleted file mode 100644 index 8001e49a..00000000 --- a/lib/blocs/bak/search_result.dart +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Created with Android Studio. - * User: 一晟 - * Date: 2019/4/28 - * Time: 7:11 PM - * email: zhu.yan@alibaba-inc.com - * tartget: - */ -class SearchResult { - String title; - String source; - - SearchResult({this.title, this.source}); -} \ No newline at end of file diff --git a/lib/blocs/bak/search_state.dart b/lib/blocs/bak/search_state.dart deleted file mode 100644 index 85d85791..00000000 --- a/lib/blocs/bak/search_state.dart +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Created with Android Studio. - * User: 一晟 - * Date: 2019/4/28 - * Time: 7:13 PM - * email: zhu.yan@alibaba-inc.com - * tartget: - */ -import './search_result.dart'; - -abstract class SearchState {} - -class SearchError extends SearchState { - @override - String toString() => 'SearchError:获取失败'; -} - -class SearchUninitialized extends SearchState { - @override - String toString() => 'SearchUninitialized:未初始化'; -} - -class SearchLoading extends SearchState { - @override - String toString() => 'SearchLoading :正在加载'; -} - -class SearchLoaded extends SearchState { - final List res; - - SearchLoaded({ - this.res, - }); - - @override - String toString() => 'SearchLoaded:加载完毕'; -} \ No newline at end of file diff --git a/lib/blocs/bak/search_widget.dart b/lib/blocs/bak/search_widget.dart deleted file mode 100644 index 1e29848a..00000000 --- a/lib/blocs/bak/search_widget.dart +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Created with Android Studio. - * User: 一晟 - * Date: 2019/4/28 - * Time: 7:19 PM - * email: zhu.yan@alibaba-inc.com - * tartget: - */ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -//import 'package:suiyi/blocs/search/bloc.dart'; -import './search_event.dart'; -import './search_state.dart'; -import './search_bloc.dart'; - - -class SearchWidget extends StatefulWidget { - final SearchDelegate delegate; - final String query; - SearchWidget({this.delegate, this.query}); - @override - _SearchWidgetState createState() => _SearchWidgetState(); -} - -class _SearchWidgetState extends State { - final SearchBloc _search = SearchBloc(); - String old; - @override - void dispose() { - _search.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - print('1:${old}'); - print('2:${widget.query}'); - if (old != widget.query) { - _search.dispatch(SearchFetch(query: widget.query)); - old = widget.query; - } - return BlocBuilder( - bloc: _search, - builder: (BuildContext context, SearchState state) { - print('-------${state}'); - if (state is SearchUninitialized || state is SearchLoading) { - return Center( - child: CircularProgressIndicator(), - ); - } else if (state is SearchError) { - return Center( - child: Text('获取失败'), - ); - } else if (state is SearchLoaded) { - return ListView.builder( - itemBuilder: (BuildContext context, int index) { - return ListTile( - dense: true, - leading: Icon( - Icons.bookmark_border, - size: 32, - ), - title: Text( - state.res[index].title, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text(state.res[index].source), - onTap: () { - // 在这里对选中的结果进行解析,因为我目前是用golang实现的,所以就没贴代码了。 - print(state.res[index].source); - }, - ); - }, - itemCount: state.res.length, - ); - } - }, - ); - } -} \ No newline at end of file diff --git a/lib/blocs/industry_api.dart b/lib/blocs/industry_api.dart index fc8b2967..7b271ca0 100644 --- a/lib/blocs/industry_api.dart +++ b/lib/blocs/industry_api.dart @@ -1,16 +1,14 @@ -/** - * Created with Android Studio. - * User: 一晟 - * Date: 2019/4/28 - * Time: 3:20 PM - * email: zhu.yan@alibaba-inc.com - * tartget: FlatButton 的示例 - */ +// +// Created with Android Studio. +// User: 一晟 +// Date: 2019/4/28 +// Time: 3:20 PM +// email: zhu.yan@alibaba-inc.com +// tartget: FlatButton 的示例 +// import 'dart:async'; import 'package:dio/dio.dart'; -import 'dart:convert'; import 'package:html/parser.dart' show parse; -import './industry_model.dart'; import './search_result.dart'; var dio = new Dio(); diff --git a/lib/blocs/industry_bloc.dart b/lib/blocs/industry_bloc.dart index 3bcd464c..1757f3b4 100644 --- a/lib/blocs/industry_bloc.dart +++ b/lib/blocs/industry_bloc.dart @@ -23,7 +23,7 @@ class SuggestionBloc extends Bloc { try { yield SuggestionLoading(); final res = await api.suggestion(event.query); - print('res====>${res}'); + // print('res====>${res}'); yield SuggestionLoaded(res: res); } catch (_) { yield SuggestionError(); diff --git a/lib/blocs/industry_event.dart b/lib/blocs/industry_event.dart index 641720b2..b54e7940 100644 --- a/lib/blocs/industry_event.dart +++ b/lib/blocs/industry_event.dart @@ -1,10 +1,10 @@ -/** - * Created with Android Studio. - * User: 一晟 - * Date: 2019/4/28 - * Time: 3:35 PM - * email: zhu.yan@alibaba-inc.com - */ +/// +/// Created with Android Studio. +/// User: 一晟 +/// Date: 2019/4/28 +/// Time: 3:35 PM +/// email: zhu.yan@alibaba-inc.com +/// abstract class SuggestionEvent {} class SuggestionFetch extends SuggestionEvent { diff --git a/lib/blocs/industry_main.dart b/lib/blocs/industry_main.dart index 22dfbc0a..fa49fac5 100644 --- a/lib/blocs/industry_main.dart +++ b/lib/blocs/industry_main.dart @@ -1,14 +1,14 @@ -/** - * Created with Android Studio. - * User: 一晟 - * Date: 2019/4/28 - * Time: 3:52 PM - * email: zhu.yan@alibaba-inc.com - */ +/// +/// Created with Android Studio. +/// User: 一晟 +/// Date: 2019/4/28 +/// Time: 3:52 PM +/// email: zhu.yan@alibaba-inc.com +/// import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import './industry_bloc.dart'; -import './industry_event.dart'; +/// import './industry_event.dart'; import './industry_state.dart'; final SuggestionBloc suggestion = SuggestionBloc(); @@ -22,47 +22,46 @@ class IndustryPage extends StatefulWidget { } class _IndustryState extends State { + Widget renderItem(state) { + if (state is SuggestionUninitialized) { + return Center( + child: Text('暂无内容'), + ); + } else if (state is SuggestionLoading) { + return Center( + child: CircularProgressIndicator(), + ); + } else if (state is SuggestionError) { + return Center( + child: Text('出现错误'), + ); + } else if (state is SuggestionLoaded) { + if (state.res.length == 0) { + return Center( + child: Text('没有适合的结果,更换查询条件试试'), + ); + }else { + if (widget.itemTitle is Function) { + return widget.itemTitle(state); + } + } + } + return Center( + child: Text('没有适合的结果,更换查询条件试试') + ); + } + @override Widget build(BuildContext context) { return Material( child: Column( children: [ -// TextField( -// autofocus: true, -// textAlign: TextAlign.center, -// onSubmitted: (text) { -// print('onSubmitted:${text}'); -// suggestion.dispatch(SuggestionFetch(query: text)); -// }, -// ), Expanded( child: BlocBuilder( bloc: suggestion, - builder: (BuildContext context, SuggestionState state) { - print('BlocBuilder----${state}'); - if (state is SuggestionUninitialized) { - return Center( - child: Text('暂无内容'), - ); - } else if (state is SuggestionLoading) { - return Center( - child: CircularProgressIndicator(), - ); - } else if (state is SuggestionError) { - return Center( - child: Text('出现错误'), - ); - } else if (state is SuggestionLoaded) { - if (state.res.length == 0) { - return Center( - child: Text('没有适合的结果,更换查询条件试试'), - ); - }else { - if (widget.itemTitle is Function) { - return widget.itemTitle(state); - } - } - } + builder: (BuildContext context, SuggestionState state){ + /// print('BlocBuilder----${state}'); + return renderItem(state); }, ), ), diff --git a/lib/blocs/industry_model.dart b/lib/blocs/industry_model.dart index ea6cb081..550d29c7 100644 --- a/lib/blocs/industry_model.dart +++ b/lib/blocs/industry_model.dart @@ -1,10 +1,10 @@ -/** - * Created with Android Studio. - * User: 一晟 - * Date: 2019/4/28 - * Time: 3:19 PM - * email: zhu.yan@alibaba-inc.com - */ +/// +/// Created with Android Studio. +/// User: 一晟 +/// Date: 2019/4/28 +/// Time: 3:19 PM +/// email: zhu.yan@alibaba-inc.com +/// class Suggestion { String query; List suggestions; diff --git a/lib/blocs/industry_state.dart b/lib/blocs/industry_state.dart index b129830c..8083aa80 100644 --- a/lib/blocs/industry_state.dart +++ b/lib/blocs/industry_state.dart @@ -1,10 +1,10 @@ -/** - * Created with Android Studio. - * User: 一晟 - * Date: 2019/4/28 - * Time: 3:37 PM - * email: zhu.yan@alibaba-inc.com - */ +/// +/// Created with Android Studio. +/// User: 一晟 +/// Date: 2019/4/28 +/// Time: 3:37 PM +/// email: zhu.yan@alibaba-inc.com +/// abstract class SuggestionState {} class SuggestionError extends SuggestionState { diff --git a/lib/blocs/search_result.dart b/lib/blocs/search_result.dart index 8001e49a..bf64d1b1 100644 --- a/lib/blocs/search_result.dart +++ b/lib/blocs/search_result.dart @@ -1,11 +1,11 @@ -/** - * Created with Android Studio. - * User: 一晟 - * Date: 2019/4/28 - * Time: 7:11 PM - * email: zhu.yan@alibaba-inc.com - * tartget: - */ +/// +/// Created with Android Studio. +/// User: 一晟 +/// Date: 2019/4/28 +/// Time: 7:11 PM +/// email: zhu.yan@alibaba-inc.com +/// tartget: +/// class SearchResult { String title; String source; diff --git a/lib/components/cate_card.dart b/lib/components/cate_card.dart index 4d43c5d0..aab9fa48 100644 --- a/lib/components/cate_card.dart +++ b/lib/components/cate_card.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import '../model/cat.dart'; import '../resources/widget_name_to_icon.dart'; import '../components/widget_item_container.dart'; +import '../model/widget.dart'; class CateCard extends StatefulWidget { - final Cat category; + final CategoryComponent category; CateCard({@required this.category}); @override _CateCardState createState() => _CateCardState(); @@ -13,28 +13,15 @@ class CateCard extends StatefulWidget { class _CateCardState extends State { // 一级菜单目录下的二级Cat集合 - List _firstChildList = new List(); - CatControlModel catControl = new CatControlModel(); + List _firstChildList; @override void initState() { super.initState(); - getFirstChildCategoriesByParentId(); + _firstChildList = widget.category.children; } - // 获取一层目录下的二级内容 - getFirstChildCategoriesByParentId() async { - int parentId = widget.category.id; - // 构建查询条件 - Cat childCateCondition = new Cat(parentId: parentId); - List list = await catControl.getList(childCateCondition); - if (list.isNotEmpty&&list.length>=1 && this.mounted) { - setState(() { - _firstChildList = list; - }); - } - } @override Widget build(BuildContext context) { @@ -43,7 +30,6 @@ class _CateCardState extends State { //首字母转为大写 widget.category.name.substring(0, 1), widget.category.name.substring(0, 1).toUpperCase()); - return Container( width: screenWidth, padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 10.0), @@ -119,9 +105,8 @@ class _CateCardState extends State { ), ), child: WidgetItemContainer( - categories: this._firstChildList, - columnCount: 3, - isWidgetPoint:false + commonItems: this._firstChildList, + columnCount: 3 ), ); } diff --git a/lib/components/category.dart b/lib/components/category.dart index 7c6c0715..eee1035e 100644 --- a/lib/components/category.dart +++ b/lib/components/category.dart @@ -3,16 +3,17 @@ import 'dart:async'; import 'package:flutter/material.dart'; import '../routers/application.dart'; -import '../model/cat.dart'; +/// import '../model/cat.dart'; import '../model/widget.dart'; import '../widgets/index.dart'; import '../components/widget_item_container.dart'; -enum CateOrWigdet { Cat, WidgetDemo } + class CategoryHome extends StatefulWidget { - CategoryHome(this.name); - final String name; + CategoryHome(this.token); + final String token; + @override _CategoryHome createState() => new _CategoryHome(); @@ -21,12 +22,11 @@ class CategoryHome extends StatefulWidget { class _CategoryHome extends State { String title = ''; // 显示列表 cat or widget; - List categories = []; - List widgetPoints = []; - List catHistory = new List(); + List items = []; + List widgetPoints = []; + List catHistory = new List(); + - CatControlModel catControl = new CatControlModel(); - WidgetControlModel widgetControl = new WidgetControlModel(); // 所有的可用demos; List widgetDemosList = new WidgetDemoList().getDemos(); @@ -34,80 +34,60 @@ class _CategoryHome extends State { void initState() { super.initState(); // 初始化加入顶级的name - this.getCatByName(widget.name).then((Cat cat) { - catHistory.add(cat); - searchCatOrWigdet(); + print("这是新界面的id:>>> ${widget.token}"); + + CommonItem targetGroup = Application.widgetTree.find(widget.token) ?? []; + print("targetGroup::: $targetGroup"); + + catHistory.add( + targetGroup + ); + this.setState(() { + items = targetGroup.children; }); + searchCatOrWidget(); } - Future getCatByName(String name) async { - return await catControl.getCatByName(name); - } + Future back() { - if (catHistory.length == 1) { - return Future.value(true); - } - catHistory.removeLast(); - searchCatOrWigdet(); - return Future.value(false); +// if (catHistory.length == 1) { +// return Future.value(true); +// } +// catHistory.removeLast(); +// searchCatOrWidget(); + return Future.value(true); } - void go(Cat cat) { + void go(CommonItem cat) { catHistory.add(cat); - searchCatOrWigdet(); + searchCatOrWidget(); } - void searchCatOrWigdet() async { - // 假设进入这个界面的parent一定存在 - Cat parentCat = catHistory.last; + void searchCatOrWidget() async { + /// CommonItem widgetTree = Application.widgetTree; + // 假设进入这个界面的parent一定存在 + CommonItem targetGroup = catHistory.last; - // 继续搜索显示下一级depth: depth + 1, parentId: parentCat.id - List _categories = - await catControl.getList(new Cat(parentId: parentCat.id)); - List _widgetPoints = new List(); - if (_categories.isEmpty) { - _widgetPoints = - await widgetControl.getList(new WidgetPoint(catId: parentCat.id)); - } - this.setState(() { - categories = _categories; - title = parentCat.name; - widgetPoints = _widgetPoints; + title = targetGroup.name; }); } - void onCatgoryTap(Cat cat) { + void onCatgoryTap(CommonItem cat) { go(cat); } - void onWidgetTap(WidgetPoint widgetPoint) { - String targetName = widgetPoint.name; - String targetRouter = '/category/error/404'; - widgetDemosList.forEach((item) { - // print("targetRouter = item.routerName> ${[item.name,targetName]}"); - if (item.name == targetName) { - targetRouter = item.routerName; - } - }); - Application.router.navigateTo(context, "$targetRouter"); - } + Widget _buildContent() { WidgetItemContainer wiContaienr = WidgetItemContainer( columnCount: 3, - categories: categories, - isWidgetPoint:false + commonItems: items ); - if (widgetPoints.length > 0) { - wiContaienr = WidgetItemContainer( - categories: widgetPoints, - columnCount: 3, - isWidgetPoint:true - ); - } + + return Container( padding: const EdgeInsets.only(bottom: 10.0, top: 5.0), decoration: BoxDecoration( @@ -122,14 +102,18 @@ class _CategoryHome extends State { @override Widget build(BuildContext context) { + + return Scaffold( appBar: AppBar( - title: Text(title), + title: Text("$title"), ), body: WillPopScope( + onWillPop: () { return back(); }, + child: ListView( children: [ _buildContent(), diff --git a/lib/components/disclaimer_msg.dart b/lib/components/disclaimer_msg.dart index 78ebb0a5..d7c4bf7f 100644 --- a/lib/components/disclaimer_msg.dart +++ b/lib/components/disclaimer_msg.dart @@ -9,7 +9,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; - const disclaimerText1 = '\r\r\r\r\r\r本APP属于个人的非赢利性开源项目,以供开源社区使用,凡本APP转载的所有的文章 、图片、音频、视频文件等资料的版权归版权所有人所有,本APP采用的非本站原创文章及图片等内容无法一一和版权者联系,如果本网所选内容的文章作者及编辑认为其作品不宜上网供大家浏览,或不应无偿使用请及时用电子邮件或电话通知我们,以迅速采取适当措施,避免给双方造成不必要的经济损失。'; const disclaimerText2 = @@ -23,6 +22,7 @@ class DisclaimerMsg extends StatefulWidget { DisclaimerMsgState createState() => DisclaimerMsgState(); } + class DisclaimerMsgState extends State { Future _prefs = SharedPreferences.getInstance(); Future _unKnow; @@ -33,11 +33,14 @@ class DisclaimerMsgState extends State { void refs(bool value) async { final SharedPreferences prefs = await _prefs; final bool unKnow = value; + _valBool = value; + _readed = value; if (mounted) { setState(() { - _unKnow = prefs.setBool("disclaimer::Boolean", unKnow).then((bool success) { - return unKnow; - }); + _unKnow = + prefs.setBool("disclaimer::Boolean", unKnow).then((bool success) { + return unKnow; + }); }); } } @@ -60,110 +63,14 @@ class DisclaimerMsgState extends State { context: context, barrierDismissible: false, // user must tap button! builder: (BuildContext context) { - return AlertDialog( - //title: Text('免责声明'), - content: SingleChildScrollView( - child: ListBody( - children: [ - Container( - padding: EdgeInsets.fromLTRB(5.0, 5.0, 10.0, 10.0), - //width: 100, - height: 35, - child: Text('免责声明', - style: TextStyle( - fontSize: 18, fontWeight: FontWeight.w700)), - decoration: BoxDecoration( - //color: Colors.blue, - image: DecorationImage( - fit: BoxFit.fitWidth, - image: AssetImage('assets/images/paimaiLogo.png')), - borderRadius: BorderRadius.all( - Radius.circular(10.0), - ), - //alignment: Alignment.bottomRight, - )), - SizedBox(height: 20), - Text(disclaimerText1), - Text(disclaimerText2), - ], - ), - ), - shape: RoundedRectangleBorder( - borderRadius: new BorderRadius.circular(20.0)), // 圆角 - - actions: [ - new Container( - width: 250, - child: _create(), - ) - ], - ); + return DisclaimerMsgDialog(_valBool, _readed, (b){ + refs(b); + }); }, ); } - Row _create() { - //已读 - if (_readed) { - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FlatButton( - padding: EdgeInsets.symmetric(horizontal: 20.0), - child: Text('已阅读知晓', - style: TextStyle(fontSize: 16, color: Colors.white)), - //可点击 - color: Theme.of(context).primaryColor, - onPressed: () { - Navigator.of(context).pop(); - }, - ), - SizedBox( - width: 10.0, - ) - ], - ); - } - //第一次读取 - return Row(mainAxisAlignment: MainAxisAlignment.spaceAround, - //crossAxisAlignment:CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Checkbox( - activeColor: Theme.of(context).primaryColor, - tristate: false, - value: _valBool, - onChanged: (bool bol) { - if(mounted) { - setState(() { - _valBool = bol; - }); - } - Navigator.of(context).pop(); // here I pop to avoid multiple Dialogs - showAlertDialog(context); //here i call the same function - }), - Text('不再自动提示', style: TextStyle(fontSize: 14)), - ], - ), - FlatButton( - child: Text('知道了', - style: TextStyle(fontSize: 16, color: Colors.white)), - //可点击 - color: _valBool - ? Theme.of(context).primaryColor - : Theme.of(context).primaryColor.withAlpha(800), - onPressed: () { - // if (_valBool) { - refs(_valBool); - Navigator.of(context).pop(); - // } - }, - ), - ]); - } Widget build(BuildContext context) { return GestureDetector( @@ -178,7 +85,7 @@ class DisclaimerMsgState extends State { alignment: Alignment.center, decoration: BoxDecoration( borderRadius: - BorderRadius.horizontal(right: Radius.circular(10)), + BorderRadius.horizontal(right: Radius.circular(10)), color: Colors.black45, ), child: Text( @@ -194,3 +101,150 @@ class DisclaimerMsgState extends State { )); } } + + +class DisclaimerMsgDialog extends StatefulWidget { + + final bool valBool; + final bool readed; + final ValueChanged onValueChanged; + + + DisclaimerMsgDialog(this.valBool, this.readed, this.onValueChanged); + + @override + _DisclaimerMsgDialogState createState() => _DisclaimerMsgDialogState(); +} + +class _DisclaimerMsgDialogState extends State { + + bool readBool; + + + @override + void initState() { + super.initState(); + readBool = widget.valBool; + } + + + + Row _create() { + //已读 + if (widget.readed) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FlatButton( + padding: EdgeInsets.symmetric(horizontal: 20.0), + child: Text('已阅读知晓', + style: TextStyle(fontSize: 16, color: Colors.white)), + //可点击 + color: Theme + .of(context) + .primaryColor, + onPressed: () { + Navigator.of(context).pop(); + }, + ), + SizedBox( + width: 10.0, + ) + ], + ); + } + + /// 选中状态更新,并返回数据 + checkChanged(){ + if (mounted) { + setState(() { + readBool = !readBool; + }); + } + } + + //第一次读取 + return Row(mainAxisAlignment: MainAxisAlignment.spaceAround, + //crossAxisAlignment:CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: (){ + checkChanged(); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Checkbox( + activeColor: Theme + .of(context) + .primaryColor, + tristate: false, + value: readBool, + onChanged: (bool bol) { + checkChanged(); + }), + Text('不再自动提示', style: TextStyle(fontSize: 14)), + ], + ), + ), + FlatButton( + child: Text('知道了', + style: TextStyle(fontSize: 16, color: Colors.white)), + //可点击 + color: readBool + ? Theme + .of(context) + .primaryColor + : Theme + .of(context) + .primaryColor + .withAlpha(800), + onPressed: () { + widget.onValueChanged(readBool); + Navigator.of(context).pop(); + }, + ), + ]); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + //title: Text('免责声明'), + content: SingleChildScrollView( + child: ListBody( + children: [ + Container( + padding: EdgeInsets.fromLTRB(5.0, 5.0, 10.0, 10.0), + //width: 100, + height: 35, + child: Text('免责声明', + style: + TextStyle(fontSize: 18, fontWeight: FontWeight.w700)), + decoration: BoxDecoration( + //color: Colors.blue, + image: DecorationImage( + fit: BoxFit.fitWidth, + image: AssetImage('assets/images/paimaiLogo.png')), + borderRadius: BorderRadius.all( + Radius.circular(10.0), + ), + //alignment: Alignment.bottomRight, + )), + SizedBox(height: 20), + Text(disclaimerText1), + Text(disclaimerText2), + ], + ), + ), + shape: RoundedRectangleBorder( + borderRadius: new BorderRadius.circular(20.0)), // 圆角 + + actions: [ + new Container( + width: 250, + child: _create(), + ) + ], + ); + }} diff --git a/lib/components/flutter_markdown/LICENSE b/lib/components/flutter_markdown/LICENSE new file mode 100644 index 00000000..e7892520 --- /dev/null +++ b/lib/components/flutter_markdown/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2017 Google, Inc. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/components/flutter_markdown/README.md b/lib/components/flutter_markdown/README.md new file mode 100644 index 00000000..35c4a267 --- /dev/null +++ b/lib/components/flutter_markdown/README.md @@ -0,0 +1,39 @@ +# Flutter Markdown +[![pub package](https://img.shields.io/pub/v/flutter_markdown.svg)](https://pub.dartlang.org/packages/flutter_markdown) +[![Build Status](https://travis-ci.org/flutter/flutter_markdown.svg?branch=master)](https://travis-ci.org/flutter/flutter_markdown) + + +A markdown renderer for Flutter. It supports the +[original format](https://daringfireball.net/projects/markdown/), but no inline +html. + +## Getting Started + +Using the Markdown widget is simple, just pass in the source markdown as a +string: + + new Markdown(data: markdownSource); + +If you do not want the padding or scrolling behavior, use the MarkdownBody +instead: + + new MarkdownBody(data: markdownSource); + +By default, Markdown uses the formatting from the current material design theme, +but it's possible to create your own custom styling. Use the MarkdownStyle class +to pass in your own style. If you don't want to use Markdown outside of material +design, use the MarkdownRaw class. + +## Image support + +The `Img` tag only supports the following image locations: + +* From the network: Use a URL prefixed by either `http://` or `https://`. + +* From local files on the device: Use an absolute path to the file, for example by + concatenating the file name with the path returned by a known storage location, + such as those provided by the [`path_provider`](https://pub.dartlang.org/packages/path_provider) + plugin. + +* From image locations referring to bundled assets: Use an asset name prefixed by `resource:`. + like `resource:assets/image.png`. diff --git a/lib/components/flutter_markdown/lib/flutter_markdown.dart b/lib/components/flutter_markdown/lib/flutter_markdown.dart new file mode 100644 index 00000000..8d7ed6ea --- /dev/null +++ b/lib/components/flutter_markdown/lib/flutter_markdown.dart @@ -0,0 +1,10 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// A library to render markdown formatted text. +library flutter_markdown; + +export 'src/builder.dart'; +export 'src/style_sheet.dart'; +export 'src/widget.dart'; diff --git a/lib/components/flutter_markdown/lib/src/builder.dart b/lib/components/flutter_markdown/lib/src/builder.dart new file mode 100644 index 00000000..089e83c3 --- /dev/null +++ b/lib/components/flutter_markdown/lib/src/builder.dart @@ -0,0 +1,376 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; +import 'package:markdown/markdown.dart' as md; +import 'package:path/path.dart' as p; +import 'style_sheet.dart'; + +typedef Widget DemoBuilder(Map attrs); + +final Set _kBlockTags = new Set.from([ + 'p', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'li', + 'blockquote', + 'pre', + 'ol', + 'ul', + 'hr', +]); + +const List _kListTags = const ['ul', 'ol']; + +bool _isBlockTag(String tag) => _kBlockTags.contains(tag); +bool _isListTag(String tag) => _kListTags.contains(tag); + +class _BlockElement { + _BlockElement(this.tag); + + final String tag; + final List children = []; + + int nextListIndex = 0; +} + +/// A collection of widgets that should be placed adjacent to (inline with) +/// other inline elements in the same parent block. +/// +/// Inline elements can be textual (a/em/strong) represented by [RichText] +/// widgets or images (img) represented by [Image.network] widgets. +/// +/// Inline elements can be nested within other inline elements, inheriting their +/// parent's style along with the style of the block they are in. +/// +/// When laying out inline widgets, first, any adjacent RichText widgets are +/// merged, then, all inline widgets are enclosed in a parent [Wrap] widget. +class _InlineElement { + _InlineElement(this.tag, {this.style}); + + final String tag; + + /// Created by merging the style defined for this element's [tag] in the + /// delegate's [MarkdownStyleSheet] with the style of its parent. + final TextStyle style; + + final List children = []; +} + +/// A delegate used by [MarkdownBuilder] to control the widgets it creates. +abstract class MarkdownBuilderDelegate { + /// Returns a gesture recognizer to use for an `a` element with the given + /// `href` attribute. + GestureRecognizer createLink(String href); + + /// Returns formatted text to use to display the given contents of a `pre` + /// element. + /// + /// The `styleSheet` is the value of [MarkdownBuilder.styleSheet]. + TextSpan formatText(MarkdownStyleSheet styleSheet, String code); +} + +/// Builds a [Widget] tree from parsed Markdown. +/// +/// See also: +/// +/// * [Markdown], which is a widget that parses and displays Markdown. +class MarkdownBuilder implements md.NodeVisitor { + /// Creates an object that builds a [Widget] tree from parsed Markdown. + MarkdownBuilder({ + this.delegate, + this.styleSheet, + this.imageDirectory, + this.demoParser + }); + + /// A delegate that controls how link and `pre` elements behave. + final MarkdownBuilderDelegate delegate; + + /// Defines which [TextStyle] objects to use for each type of element. + final MarkdownStyleSheet styleSheet; + + final DemoBuilder demoParser; + /// The base directory holding images referenced by Img tags with local file paths. + final Directory imageDirectory; + + final List _listIndents = []; + final List<_BlockElement> _blocks = <_BlockElement>[]; + final List<_InlineElement> _inlines = <_InlineElement>[]; + final List _linkHandlers = []; + + + /// Returns widgets that display the given Markdown nodes. + /// + /// The returned widgets are typically used as children in a [ListView]. + List build(List nodes) { + _listIndents.clear(); + _blocks.clear(); + _inlines.clear(); + _linkHandlers.clear(); + + _blocks.add(new _BlockElement(null)); + + for (md.Node node in nodes) { + assert(_blocks.length == 1); + node.accept(this); + } + + assert(_inlines.isEmpty); + return _blocks.single.children; + } + + @override + void visitText(md.Text text) { + if (_blocks.last.tag == null) // Don't allow text directly under the root. + return; + + _addParentInlineIfNeeded(_blocks.last.tag); + + final TextSpan span = _blocks.last.tag == 'pre' + ? delegate.formatText(styleSheet, text.text) + : new TextSpan( + style: _inlines.last.style, + text: text.text, + recognizer: _linkHandlers.isNotEmpty ? _linkHandlers.last : null, + ); + + _inlines.last.children.add(new RichText( + textScaleFactor: styleSheet.textScaleFactor, + text: span, + )); + } + + @override + bool visitElementBefore(md.Element element) { +// print("visitElementBefore ${element.tag}"); + final String tag = element.tag; + if (_isBlockTag(tag)) { + _addAnonymousBlockIfNeeded(styleSheet.styles[tag]); + if (_isListTag(tag)) + _listIndents.add(tag); + _blocks.add(new _BlockElement(tag)); + } else { + _addParentInlineIfNeeded(_blocks.last.tag); + + TextStyle parentStyle = _inlines.last.style; + _inlines.add(new _InlineElement( + tag, + style: parentStyle.merge(styleSheet.styles[tag]), + )); + } + + if (tag == 'a') { + _linkHandlers.add(delegate.createLink(element.attributes['href'])); + } + return true; + } + + @override + void visitElementAfter(md.Element element) { + final String tag = element.tag; + if (_isBlockTag(tag)) { + _addAnonymousBlockIfNeeded(styleSheet.styles[tag]); + + final _BlockElement current = _blocks.removeLast(); + Widget child; + + if (current.children.isNotEmpty) { + child = new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: current.children, + ); + } else { + child = const SizedBox(); + } + + if (_isListTag(tag)) { + assert(_listIndents.isNotEmpty); + _listIndents.removeLast(); + } else if (tag == 'li') { + if (_listIndents.isNotEmpty) { + child = new Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + new SizedBox( + width: styleSheet.listIndent, + child: _buildBullet(_listIndents.last), + ), + new Expanded(child: child) + ], + ); + } + } else if (tag == 'blockquote') { + child = new DecoratedBox( + decoration: styleSheet.blockquoteDecoration, + child: new Padding( + padding: new EdgeInsets.all(styleSheet.blockquotePadding), + child: child, + ), + ); + } else if (tag == 'pre') { + child = new DecoratedBox( + decoration: styleSheet.codeblockDecoration, + child: new Padding( + padding: new EdgeInsets.all(styleSheet.codeblockPadding), + child: child, + ), + ); + } else if (tag == 'hr') { + child = new DecoratedBox( + decoration: styleSheet.horizontalRuleDecoration, + child: child, + ); + } + _addBlockChild(child); + } else { + + final _InlineElement current = _inlines.removeLast(); + final _InlineElement parent = _inlines.last; + + if (tag == 'img') { + // create an image widget for this image + current.children.add(_buildImage(element.attributes['src'])); + } else if (tag == 'a') { + _linkHandlers.removeLast(); + } else if (tag == 'demo') { + current.children.add(_buildGoDemos(element.attributes)); + } + + + if (current.children.isNotEmpty) { + parent.children.addAll(current.children); + } + } + } + Widget _buildGoDemos(Map attrs) { + Widget targetGoDemos; + + if (demoParser != null) { + targetGoDemos = demoParser(attrs); + } + + return targetGoDemos ?? new Text('demo not exits'); + } + + Widget _buildImage(String src) { + final List parts = src.split('#'); + if (parts.isEmpty) + return const SizedBox(); + + final String path = parts.first; + double width; + double height; + if (parts.length == 2) { + final List dimensions = parts.last.split('x'); + if (dimensions.length == 2) { + width = double.parse(dimensions[0]); + height = double.parse(dimensions[1]); + } + } + + Uri uri = Uri.parse(path); + Widget child; + if (uri.scheme == 'http' || uri.scheme == 'https') { + child = new Image.network(uri.toString(), width: width, height: height); + } else if (uri.scheme == 'data') { + child = _handleDataSchemeUri(uri, width, height); + } else if (uri.scheme == "resource") { + child = new Image.asset(path.substring(9), width: width, height: height); + } else { + String filePath = (imageDirectory == null + ? uri.toFilePath() + : p.join(imageDirectory.path, uri.toFilePath())); + child = new Image.file(new File(filePath), width: width, height: height); + } + + if (_linkHandlers.isNotEmpty) { + TapGestureRecognizer recognizer = _linkHandlers.last; + return new GestureDetector(child: child, onTap: recognizer.onTap); + } else { + return child; + } + } + + Widget _handleDataSchemeUri(Uri uri, final double width, final double height) { + final String mimeType = uri.data.mimeType; + if (mimeType.startsWith('image/')) { + return new Image.memory(uri.data.contentAsBytes(), width: width, height: height); + } else if (mimeType.startsWith('text/')) { + return new Text(uri.data.contentAsString()); + } + return const SizedBox(); + } + + Widget _buildBullet(String listTag) { + if (listTag == 'ul') + return new Text('•', textAlign: TextAlign.center, style: styleSheet.styles['li']); + + final int index = _blocks.last.nextListIndex; + return new Padding( + padding: const EdgeInsets.only(right: 5.0), + child: new Text('${index + 1}.', textAlign: TextAlign.right, style: styleSheet.styles['li']), + ); + } + + void _addParentInlineIfNeeded(String tag) { + if (_inlines.isEmpty) { + _inlines.add(new _InlineElement( + tag, + style: styleSheet.styles[tag], + )); + } + } + + void _addBlockChild(Widget child) { + final _BlockElement parent = _blocks.last; + if (parent.children.isNotEmpty) + parent.children.add(new SizedBox(height: styleSheet.blockSpacing)); + parent.children.add(child); + parent.nextListIndex += 1; + } + + void _addAnonymousBlockIfNeeded(TextStyle style) { + if (_inlines.isEmpty) { + return; + } + + final _InlineElement inline = _inlines.single; + if (inline.children.isNotEmpty) { + List mergedInlines = _mergeInlineChildren(inline); + final Wrap wrap = new Wrap(children: mergedInlines); + _addBlockChild(wrap); + _inlines.clear(); + } + } + + /// Merges adjacent [TextSpan] children of the given [_InlineElement] + List _mergeInlineChildren(_InlineElement inline) { + List mergedTexts = []; + for (Widget child in inline.children) { + if (mergedTexts.isNotEmpty && mergedTexts.last is RichText && child is RichText) { + RichText previous = mergedTexts.removeLast(); + List children = previous.text.children != null + ? new List.from(previous.text.children) + : [previous.text]; + children.add(child.text); + TextSpan mergedSpan = new TextSpan(children: children); + mergedTexts.add(new RichText( + textScaleFactor: styleSheet.textScaleFactor, + text: mergedSpan, + )); + } else { + mergedTexts.add(child); + } + } + return mergedTexts; + } +} diff --git a/lib/components/flutter_markdown/lib/src/style_sheet.dart b/lib/components/flutter_markdown/lib/src/style_sheet.dart new file mode 100644 index 00000000..6c44b773 --- /dev/null +++ b/lib/components/flutter_markdown/lib/src/style_sheet.dart @@ -0,0 +1,307 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Defines which [TextStyle] objects to use for which Markdown elements. +class MarkdownStyleSheet { + /// Creates an explicit mapping of [TextStyle] objects to Markdown elements. + MarkdownStyleSheet({ + this.a, + this.p, + this.code, + this.h1, + this.h2, + this.h3, + this.h4, + this.h5, + this.h6, + this.em, + this.strong, + this.blockquote, + this.img, + this.blockSpacing, + this.listIndent, + this.blockquotePadding, + this.blockquoteDecoration, + this.codeblockPadding, + this.codeblockDecoration, + this.horizontalRuleDecoration, + this.textScaleFactor = 1.0 + }) : _styles = { + 'a': a, + 'p': p, + 'li': p, + 'code': code, + 'pre': p, + 'h1': h1, + 'h2': h2, + 'h3': h3, + 'h4': h4, + 'h5': h5, + 'h6': h6, + 'em': em, + 'strong': strong, + 'blockquote': blockquote, + 'img': img, + }; + + /// Creates a [MarkdownStyleSheet] from the [TextStyle]s in the provided [ThemeData]. + factory MarkdownStyleSheet.fromTheme(ThemeData theme) { + assert(theme?.textTheme?.body1?.fontSize != null); + return new MarkdownStyleSheet( + a: const TextStyle(color: Colors.blue), + p: theme.textTheme.body1, + code: new TextStyle( + color: Colors.grey.shade700, + fontFamily: "monospace", + fontSize: theme.textTheme.body1.fontSize * 0.85 + ), + h1: theme.textTheme.headline, + h2: theme.textTheme.title, + h3: theme.textTheme.subhead, + h4: theme.textTheme.body2, + h5: theme.textTheme.body2, + h6: theme.textTheme.body2, + em: const TextStyle(fontStyle: FontStyle.italic), + strong: const TextStyle(fontWeight: FontWeight.bold), + blockquote: theme.textTheme.body1, + img: theme.textTheme.body1, + blockSpacing: 8.0, + listIndent: 32.0, + blockquotePadding: 8.0, + blockquoteDecoration: new BoxDecoration( + color: Colors.blue.shade100, + borderRadius: new BorderRadius.circular(2.0) + ), + codeblockPadding: 8.0, + codeblockDecoration: new BoxDecoration( + color: Colors.grey.shade100, + borderRadius: new BorderRadius.circular(2.0) + ), + horizontalRuleDecoration: new BoxDecoration( + border: new Border( + top: new BorderSide(width: 5.0, color: Colors.grey.shade300) + ), + ), + ); + } + + /// Creates a [MarkdownStyle] from the [TextStyle]s in the provided [ThemeData]. + /// + /// This constructor uses larger fonts for the headings than in + /// [MarkdownStyle.fromTheme]. + factory MarkdownStyleSheet.largeFromTheme(ThemeData theme) { + return new MarkdownStyleSheet( + a: const TextStyle(color: Colors.blue), + p: theme.textTheme.body1, + code: new TextStyle( + color: Colors.grey.shade700, + fontFamily: "monospace", + fontSize: theme.textTheme.body1.fontSize * 0.85 + ), + h1: theme.textTheme.display3, + h2: theme.textTheme.display2, + h3: theme.textTheme.display1, + h4: theme.textTheme.headline, + h5: theme.textTheme.title, + h6: theme.textTheme.subhead, + em: const TextStyle(fontStyle: FontStyle.italic), + strong: const TextStyle(fontWeight: FontWeight.bold), + blockquote: theme.textTheme.body1, + img: theme.textTheme.body1, + blockSpacing: 8.0, + listIndent: 32.0, + blockquotePadding: 8.0, + blockquoteDecoration: new BoxDecoration( + color: Colors.blue.shade100, + borderRadius: new BorderRadius.circular(2.0) + ), + codeblockPadding: 8.0, + codeblockDecoration: new BoxDecoration( + color: Colors.grey.shade100, + borderRadius: new BorderRadius.circular(2.0) + ), + horizontalRuleDecoration: new BoxDecoration( + border: new Border( + top: new BorderSide(width: 5.0, color: Colors.grey.shade300) + ), + ), + ); + } + + /// Creates a new [MarkdownStyleSheet] based on the current style, with the + /// provided parameters overridden. + MarkdownStyleSheet copyWith({ + TextStyle a, + TextStyle p, + TextStyle code, + TextStyle h1, + TextStyle h2, + TextStyle h3, + TextStyle h4, + TextStyle h5, + TextStyle h6, + TextStyle em, + TextStyle strong, + TextStyle blockquote, + TextStyle img, + double blockSpacing, + double listIndent, + double blockquotePadding, + Decoration blockquoteDecoration, + double codeblockPadding, + Decoration codeblockDecoration, + Decoration horizontalRuleDecoration, + double textScaleFactor, + }) { + return new MarkdownStyleSheet( + a: a ?? this.a, + p: p ?? this.p, + code: code ?? this.code, + h1: h1 ?? this.h1, + h2: h2 ?? this.h2, + h3: h3 ?? this.h3, + h4: h4 ?? this.h4, + h5: h5 ?? this.h5, + h6: h6 ?? this.h6, + em: em ?? this.em, + strong: strong ?? this.strong, + blockquote: blockquote ?? this.blockquote, + img: img ?? this.img, + blockSpacing: blockSpacing ?? this.blockSpacing, + listIndent: listIndent ?? this.listIndent, + blockquotePadding: blockquotePadding ?? this.blockquotePadding, + blockquoteDecoration: blockquoteDecoration ?? this.blockquoteDecoration, + codeblockPadding: codeblockPadding ?? this.codeblockPadding, + codeblockDecoration: codeblockDecoration ?? this.codeblockDecoration, + horizontalRuleDecoration: horizontalRuleDecoration ?? this.horizontalRuleDecoration, + textScaleFactor : textScaleFactor ?? this.textScaleFactor, + ); + } + + /// The [TextStyle] to use for `a` elements. + final TextStyle a; + + /// The [TextStyle] to use for `p` elements. + final TextStyle p; + + /// The [TextStyle] to use for `code` elements. + final TextStyle code; + + /// The [TextStyle] to use for `h1` elements. + final TextStyle h1; + + /// The [TextStyle] to use for `h2` elements. + final TextStyle h2; + + /// The [TextStyle] to use for `h3` elements. + final TextStyle h3; + + /// The [TextStyle] to use for `h4` elements. + final TextStyle h4; + + /// The [TextStyle] to use for `h5` elements. + final TextStyle h5; + + /// The [TextStyle] to use for `h6` elements. + final TextStyle h6; + + /// The [TextStyle] to use for `em` elements. + final TextStyle em; + + /// The [TextStyle] to use for `strong` elements. + final TextStyle strong; + + /// The [TextStyle] to use for `blockquote` elements. + final TextStyle blockquote; + + /// The [TextStyle] to use for `img` elements. + final TextStyle img; + + /// The amount of vertical space to use between block-level elements. + final double blockSpacing; + + /// The amount of horizontal space to indent list items. + final double listIndent; + + /// The padding to use for `blockquote` elements. + final double blockquotePadding; + + /// The decoration to use behind `blockquote` elements. + final Decoration blockquoteDecoration; + + /// The padding to use for `pre` elements. + final double codeblockPadding; + + /// The decoration to use behind for `pre` elements. + final Decoration codeblockDecoration; + + /// The decoration to use for `hr` elements. + final Decoration horizontalRuleDecoration; + + // The text scale factor to use in textual elements + final double textScaleFactor; + + /// A [Map] from element name to the corresponding [TextStyle] object. + Map get styles => _styles; + Map _styles; + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) + return true; + if (other.runtimeType != MarkdownStyleSheet) + return false; + final MarkdownStyleSheet typedOther = other; + return typedOther.a == a + && typedOther.p == p + && typedOther.code == code + && typedOther.h1 == h1 + && typedOther.h2 == h2 + && typedOther.h3 == h3 + && typedOther.h4 == h4 + && typedOther.h5 == h5 + && typedOther.h6 == h6 + && typedOther.em == em + && typedOther.strong == strong + && typedOther.blockquote == blockquote + && typedOther.img == img + && typedOther.blockSpacing == blockSpacing + && typedOther.listIndent == listIndent + && typedOther.blockquotePadding == blockquotePadding + && typedOther.blockquoteDecoration == blockquoteDecoration + && typedOther.codeblockPadding == codeblockPadding + && typedOther.codeblockDecoration == codeblockDecoration + && typedOther.horizontalRuleDecoration == horizontalRuleDecoration + && typedOther.textScaleFactor == textScaleFactor; + } + + @override + int get hashCode { + return hashList([ + a, + p, + code, + h1, + h2, + h3, + h4, + h5, + h6, + em, + strong, + blockquote, + img, + blockSpacing, + listIndent, + blockquotePadding, + blockquoteDecoration, + codeblockPadding, + codeblockDecoration, + horizontalRuleDecoration, + textScaleFactor, + ]); + } +} diff --git a/lib/components/flutter_markdown/lib/src/widget.dart b/lib/components/flutter_markdown/lib/src/widget.dart new file mode 100644 index 00000000..c9d5cbaf --- /dev/null +++ b/lib/components/flutter_markdown/lib/src/widget.dart @@ -0,0 +1,247 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:markdown/markdown.dart' as md; +import 'package:meta/meta.dart'; + +import 'builder.dart'; +import 'style_sheet.dart'; +// +typedef Widget ItemDemoBuilder(Map attrs); + +/// Signature for callbacks used by [MarkdownWidget] when the user taps a link. +/// +/// Used by [MarkdownWidget.onTapLink]. +typedef void MarkdownTapLinkCallback(String href); + +/// Creates a format [TextSpan] given a string. +/// +/// Used by [MarkdownWidget] to highlight the contents of `pre` elements. +abstract class SyntaxHighlighter { // ignore: one_member_abstracts + /// Returns the formated [TextSpan] for the given string. + TextSpan format(String source); +} + +/// A base class for widgets that parse and display Markdown. +/// +/// Supports all standard Markdown from the original +/// [Markdown specification](https://daringfireball.net/projects/markdown/). +/// +/// See also: +/// +/// * [Markdown], which is a scrolling container of Markdown. +/// * [MarkdownBody], which is a non-scrolling container of Markdown. +/// * +abstract class MarkdownWidget extends StatefulWidget { + /// Creates a widget that parses and displays Markdown. + /// + /// The [data] argument must not be null. + const MarkdownWidget({ + Key key, + @required this.data, + this.styleSheet, + this.syntaxHighlighter, + this.onTapLink, + this.imageDirectory, + this.demoBuilder, + }) : assert(data != null), + super(key: key); + + /// The Markdown to display. + final String data; + + /// The styles to use when displaying the Markdown. + /// + /// If null, the styles are inferred from the current [Theme]. + final MarkdownStyleSheet styleSheet; + + /// The syntax highlighter used to color text in `pre` elements. + /// + /// If null, the [MarkdownStyleSheet.code] style is used for `pre` elements. + final SyntaxHighlighter syntaxHighlighter; + + /// Called when the user taps a link. + final MarkdownTapLinkCallback onTapLink; + + /// The base directory holding images referenced by Img tags with local file paths. + final Directory imageDirectory; + + final ItemDemoBuilder demoBuilder; + /// Subclasses should override this function to display the given children, + /// which are the parsed representation of [data]. + @protected + Widget build(BuildContext context, List children); + + @override + _MarkdownWidgetState createState() => new _MarkdownWidgetState(); +} + +class DemosSyntax extends md.InlineSyntax { + DemosSyntax() : super('\\[demo:([a-z0-9_+-]+)\\]'); + bool onMatch(parser, match) { + var anchor = new md.Element.empty('demo'); + anchor.attributes['id'] = match[1]; + parser.addNode(anchor); + return true; + } +} + +class _MarkdownWidgetState extends State implements MarkdownBuilderDelegate { + List _children; + final List _recognizers = []; + + @override + void didChangeDependencies() { + _parseMarkdown(); + super.didChangeDependencies(); + } + + @override + void didUpdateWidget(MarkdownWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.data != oldWidget.data + || widget.styleSheet != oldWidget.styleSheet) + _parseMarkdown(); + } + + @override + void dispose() { + _disposeRecognizers(); + super.dispose(); + } + + void _parseMarkdown() { + final MarkdownStyleSheet styleSheet = widget.styleSheet ?? new MarkdownStyleSheet.fromTheme(Theme.of(context)); + + _disposeRecognizers(); + + // TODO: This can be optimized by doing the split and removing \r at the same time + final List lines = widget.data.replaceAll('\r\n', '\n').split('\n'); + final md.ExtensionSet extens = new md.ExtensionSet([ + md.FencedCodeBlockSyntax() + ], [ + new DemosSyntax(), + new md.InlineHtmlSyntax(), + ]); + final md.Document document = new md.Document(encodeHtml: false, extensionSet: extens); + final MarkdownBuilder builder = new MarkdownBuilder( + delegate: this, + styleSheet: styleSheet, + imageDirectory: widget.imageDirectory, + demoParser: widget.demoBuilder + ); + _children = builder.build(document.parseLines(lines)); + } + + void _disposeRecognizers() { + if (_recognizers.isEmpty) + return; + final List localRecognizers = new List.from(_recognizers); + _recognizers.clear(); + for (GestureRecognizer recognizer in localRecognizers) + recognizer.dispose(); + } + + @override + GestureRecognizer createLink(String href) { + final TapGestureRecognizer recognizer = new TapGestureRecognizer() + ..onTap = () { + if (widget.onTapLink != null) + widget.onTapLink(href); + }; + _recognizers.add(recognizer); + return recognizer; + } + + @override + TextSpan formatText(MarkdownStyleSheet styleSheet, String code) { + if (widget.syntaxHighlighter != null) + return widget.syntaxHighlighter.format(code); + return new TextSpan(style: styleSheet.code, text: code); + } + + @override + Widget build(BuildContext context) => widget.build(context, _children); +} + +/// A non-scrolling widget that parses and displays Markdown. +/// +/// Supports all standard Markdown from the original +/// [Markdown specification](https://daringfireball.net/projects/markdown/). +/// +/// See also: +/// +/// * [Markdown], which is a scrolling container of Markdown. +/// * +class MarkdownBody extends MarkdownWidget { + /// Creates a non-scrolling widget that parses and displays Markdown. + const MarkdownBody({ + Key key, + String data, + MarkdownStyleSheet styleSheet, + SyntaxHighlighter syntaxHighlighter, + MarkdownTapLinkCallback onTapLink, + Directory imageDirectory, + ItemDemoBuilder demoBuilder, + }) : super( + key: key, + data: data, + styleSheet: styleSheet, + syntaxHighlighter: syntaxHighlighter, + onTapLink: onTapLink, + imageDirectory: imageDirectory, + demoBuilder: demoBuilder + ); + + @override + Widget build(BuildContext context, List children) { + if (children.length == 1) + return children.single; + return new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: children, + ); + } +} + +/// A scrolling widget that parses and displays Markdown. +/// +/// Supports all standard Markdown from the original +/// [Markdown specification](https://daringfireball.net/projects/markdown/). +/// +/// See also: +/// +/// * [MarkdownBody], which is a non-scrolling container of Markdown. +/// * +class Markdown extends MarkdownWidget { + /// Creates a scrolling widget that parses and displays Markdown. + const Markdown({ + Key key, + String data, + MarkdownStyleSheet styleSheet, + SyntaxHighlighter syntaxHighlighter, + MarkdownTapLinkCallback onTapLink, + Directory imageDirectory, + this.padding: const EdgeInsets.all(16.0), + }) : super( + key: key, + data: data, + styleSheet: styleSheet, + syntaxHighlighter: syntaxHighlighter, + onTapLink: onTapLink, + imageDirectory: imageDirectory, + ); + + /// The amount of space by which to inset the children. + final EdgeInsets padding; + + @override + Widget build(BuildContext context, List children) { + return new ListView(padding: padding, children: children); + } +} diff --git a/lib/components/full_screen_code_dialog.dart b/lib/components/full_screen_code_dialog.dart index 76147749..5a40d9cf 100644 --- a/lib/components/full_screen_code_dialog.dart +++ b/lib/components/full_screen_code_dialog.dart @@ -7,11 +7,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_go/utils/example_code_parser.dart'; import 'package:flutter_go/utils/syntax_highlighter.dart'; +import 'package:flutter_go/utils/net_utils.dart'; class FullScreenCodeDialog extends StatefulWidget { - const FullScreenCodeDialog({this.filePath}); + const FullScreenCodeDialog({this.filePath, this.remoteFilePath}); final String filePath; + final String remoteFilePath; _FullScreenCodeDialogState createState() => _FullScreenCodeDialogState(); } @@ -21,17 +23,31 @@ class _FullScreenCodeDialogState extends State { @override void didChangeDependencies() { print('widget.filePath=======${widget.filePath}'); - getExampleCode(context,'${widget.filePath}', DefaultAssetBundle.of(context)) - .then((String code) { - if (mounted) { - setState(() { - _exampleCode = code ?? 'Example code not found'; - }); - } - }); + if (widget.filePath != null) { + getExampleCode(context,'${widget.filePath}', DefaultAssetBundle.of(context)) + .then((String code) { + if (mounted) { + setState(() { + _exampleCode = code ?? 'Example code not found'; + }); + } + }); + } + if (widget.remoteFilePath != null) { + getRemotePathCode(widget.remoteFilePath); + } + super.didChangeDependencies(); } + getRemotePathCode(path) async { + String response = await NetUtils.get(path); + if (mounted) { + setState(() { + _exampleCode = response ?? 'Example code not found'; + }); + } + } @override Widget build(BuildContext context) { final SyntaxHighlighterStyle style = diff --git a/lib/components/list_refresh.dart b/lib/components/list_refresh.dart index b4a13244..c5d3b0d8 100644 --- a/lib/components/list_refresh.dart +++ b/lib/components/list_refresh.dart @@ -174,6 +174,7 @@ class _ListRefreshState extends State { return widget.renderItem(index, items[index]); } } + return null; }, controller: _scrollController, ), diff --git a/lib/components/loading.dart b/lib/components/loading.dart new file mode 100644 index 00000000..659b2ff8 --- /dev/null +++ b/lib/components/loading.dart @@ -0,0 +1,94 @@ +// +// Created with Android Studio. +// User: 三帆 +// Date: 07/08/2019 +// Time: 08:40 +// email: sanfan.hx@alibaba-inc.com +// tartget: 代码获取自: https://blog.csdn.net/O_time/article/details/86496537 +// +import 'dart:async'; +import 'package:flutter/material.dart'; + +// ignore: must_be_immutable +class NetLoadingDialog extends StatefulWidget { + String loadingText; + bool outsideDismiss; + bool loading; + Function dismissCallback; + Future requestCallBack; + + NetLoadingDialog( + {Key key, + this.loadingText = "loading...", + this.outsideDismiss = true, + this.dismissCallback, + this.loading, + this.requestCallBack}) + : super(key: key); + + @override + State createState() => _LoadingDialog(); +} + +class _LoadingDialog extends State { + _dismissDialog() { + if (widget.dismissCallback != null) { + widget.dismissCallback(); + } + Navigator.of(context).pop(); + } + + @override + void initState() { + super.initState(); + if (widget.requestCallBack != null) { + widget.requestCallBack.then((_) { + Navigator.pop(context); + }); + } + } + + @override + Widget build(BuildContext context) { + if (!widget.loading) { + return Container(); + } + return new GestureDetector( + onTap: widget.outsideDismiss ? _dismissDialog : null, + child: Material( + type: MaterialType.transparency, + child: new Center( + child: new SizedBox( + width: 120.0, + height: 120.0, + child: new Container( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(8.0), + ), + ), + ), + child: new Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + new CircularProgressIndicator(), + new Padding( + padding: const EdgeInsets.only( + top: 20.0, + ), + child: new Text( + widget.loadingText, + style: new TextStyle(fontSize: 12.0), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/components/markdown.dart b/lib/components/markdown.dart index 02cf6af7..af055381 100644 --- a/lib/components/markdown.dart +++ b/lib/components/markdown.dart @@ -1,4 +1,4 @@ -import 'package:flutter_markdown/flutter_markdown.dart' as md; +import '../components/flutter_markdown/lib/flutter_markdown.dart' as md; import 'package:flutter/material.dart'; import 'package:flutter_go/utils/high_light_code.dart'; diff --git a/lib/components/search_input.dart b/lib/components/search_input.dart index b0abdf03..9852c83a 100644 --- a/lib/components/search_input.dart +++ b/lib/components/search_input.dart @@ -15,13 +15,9 @@ typedef void OnSubmit(String value); ///搜索结果内容显示面板 class MaterialSearchResult extends StatelessWidget { - const MaterialSearchResult({ - Key key, - this.value, - this.text, - this.icon, - this.onTap - }) : super(key: key); + const MaterialSearchResult( + {Key key, this.value, this.text, this.icon, this.onTap}) + : super(key: key); final String value; final VoidCallback onTap; @@ -30,7 +26,6 @@ class MaterialSearchResult extends StatelessWidget { @override Widget build(BuildContext context) { - return new InkWell( onTap: this.onTap, child: new Container( @@ -38,8 +33,14 @@ class MaterialSearchResult extends StatelessWidget { padding: EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 10.0), child: new Row( children: [ - new Container(width: 30.0, margin: EdgeInsets.only(right: 10), child: new Icon(icon)) ?? null, - new Expanded(child: new Text(value, style: Theme.of(context).textTheme.subhead)), + new Container( + width: 30.0, + margin: EdgeInsets.only(right: 10), + child: new Icon(icon)) ?? + null, + new Expanded( + child: new Text(value, + style: Theme.of(context).textTheme.subhead)), new Text(text, style: Theme.of(context).textTheme.subhead) ], ), @@ -124,6 +125,7 @@ class _MaterialSearchState extends State { } Timer _resultsTimer; + Future _getResultsDebounced() async { if (_results.length == 0) { setState(() { @@ -164,6 +166,7 @@ class _MaterialSearchState extends State { super.dispose(); _resultsTimer?.cancel(); } + Widget buildBody(List results) { if (_criteria.isEmpty) { return History(); @@ -171,16 +174,11 @@ class _MaterialSearchState extends State { return new Center( child: new Padding( padding: const EdgeInsets.only(top: 50.0), - child: new CircularProgressIndicator() - ) - ); + child: new CircularProgressIndicator())); } if (results.isNotEmpty) { - var content = new SingleChildScrollView( - child: new Column( - children: results - ) - ); + var content = + new SingleChildScrollView(child: new Column(children: results)); return content; } return Center(child: Text("暂无数据")); @@ -241,7 +239,7 @@ class _MaterialSearchState extends State { ], ), body: buildBody(results), - ); + ); } } @@ -405,34 +403,35 @@ class SearchInput extends StatelessWidget { } // wigdet干掉.=> componets - class History extends StatefulWidget { const History() : super(); @override - _History createState() => _History(); + _History createState() => _History(); } // AppBar 默认的实例,有状态 class _History extends State { SearchHistoryList searchHistoryList = new SearchHistoryList(); + bool refreshFlag; @override void initState() { super.initState(); + this.refreshFlag = true; } @override void dispose() { super.dispose(); } + buildChips(BuildContext context) { List list = []; List historyList = searchHistoryList.getList(); print("historyList> $historyList"); Color bgColor = Theme.of(context).primaryColor; historyList.forEach((SearchHistory value) { - Widget icon = CircleAvatar( backgroundColor: bgColor, child: Text( @@ -443,21 +442,30 @@ class _History extends State { if (WidgetName2Icon.icons[value.name] != null) { icon = Icon(WidgetName2Icon.icons[value.name], size: 25); } + String targetRouter = value.targetRouter; - list.add( - InkWell( - onTap: () { - Application.router.navigateTo(context, "${value.targetRouter}", transition: TransitionType.inFromRight); - }, - child: Chip( - avatar: icon, - label: Text("${value.name}"), - ), - ) - ); + list.add(InkWell( + onTap: () { + Application.router.navigateTo( + context, "${targetRouter.toLowerCase()}", + transition: TransitionType.inFromRight); + }, + child: Chip( + avatar: icon, + label: Text("${value.name}"), + ), + )); }); return list; } + + _clearHistory() { + searchHistoryList.clear(); + this.setState(() { + this.refreshFlag = !this.refreshFlag; + }); + } + @override Widget build(BuildContext context) { List childList = buildChips(context); @@ -469,25 +477,38 @@ class _History extends State { return Column( children: [ Container( - alignment: Alignment.centerLeft, - padding: EdgeInsets.fromLTRB(12.0, 12, 12, 0), - child: InkWell( - onLongPress: () { - searchHistoryList.clear(); - }, - child: Text('历史搜索'), - ), - ), + alignment: Alignment.centerLeft, + padding: EdgeInsets.fromLTRB(12.0, 12, 12, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + InkWell( + onLongPress: () { + searchHistoryList.clear(); + }, + child: Text('历史搜索'), + ), + GestureDetector( + onTap: _clearHistory, + child: Container( + child: new Icon(Icons.delete, + size: 24.0, color: Theme.of(context).accentColor), + width: 30, + height: 30, + ), + ) + ], + )), Container( padding: EdgeInsets.only(left: 10), alignment: Alignment.topLeft, child: Wrap( - spacing: 6.0, // gap between adjacent chips - runSpacing: 0.0, // gap between lines - children: childList - ), + spacing: 6.0, // gap between adjacent chips + runSpacing: 0.0, // gap between lines + children: childList), ) ], ); } -} \ No newline at end of file +} diff --git a/lib/components/single_theme_color.dart b/lib/components/single_theme_color.dart new file mode 100644 index 00000000..8a9088a0 --- /dev/null +++ b/lib/components/single_theme_color.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_go/event/event_bus.dart'; +import 'package:flutter_go/event/event_model.dart'; +// import 'package:event_bus/event_bus.dart'; + +class SingleThemeColor extends StatelessWidget { + final int themeColor; + final String coloeName; + + const SingleThemeColor({Key key, this.themeColor, this.coloeName}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: (){ + if(ApplicationEvent.event != null){ + print('fire ${this.themeColor}'); + ApplicationEvent.event.fire(UserSettingThemeColorEvent(this.themeColor)); + Navigator.of(context).pop(); + } + }, + child: Column( + children: [ + Container( + width: 50, + height: 50, + margin: const EdgeInsets.all(5.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(50), + ), + color: Color(this.themeColor), + ), + ), + Text( + this.coloeName, + style: TextStyle( + color: Color(this.themeColor), + fontSize: 14.0, + ), + ) + ], + ), + ); + } +} diff --git a/lib/components/widget_demo.dart b/lib/components/widget_demo.dart index bbce9f51..1e2d5744 100644 --- a/lib/components/widget_demo.dart +++ b/lib/components/widget_demo.dart @@ -4,11 +4,12 @@ import 'dart:core'; import 'package:flutter/material.dart'; +import 'package:flutter_go/utils/data_utils.dart'; import '../routers/application.dart'; import '../routers/routers.dart'; import '../components/markdown.dart'; -import '../model/collection.dart'; +/// import '../model/collection.dart'; import '../widgets/index.dart'; import '../event/event_bus.dart'; import '../event/event_model.dart'; @@ -24,8 +25,8 @@ class WidgetDemo extends StatefulWidget { {Key key, @required this.title, @required this.contentList, - @required this.codeUrl, - @required this.docUrl, + this.codeUrl, + this.docUrl, this.bottomNaviBar}) : super(key: key); @@ -34,10 +35,10 @@ class WidgetDemo extends StatefulWidget { class _WidgetDemoState extends State { bool _hasCollected = false; - CollectionControlModel _collectionControl = new CollectionControlModel(); + /// CollectionControlModel _collectionControl = new CollectionControlModel(); var _collectionIcons; List widgetDemosList = new WidgetDemoList().getDemos(); - String _router = ''; + String widgetType = 'old'; final GlobalKey _scaffoldKey = GlobalKey(); List _buildContent() { @@ -64,56 +65,67 @@ class _WidgetDemoState extends State { @override void initState() { super.initState(); - _collectionControl.getRouterByName(widget.title).then((list) { - widgetDemosList.forEach((item) { - if (item.name == widget.title) { - _router = item.routerName; + // 这里不能直接 使用 ` ModalRoute.of(context)` 会产生报错 + Future.delayed(Duration.zero, () { + String currentPath = ModalRoute.of(context).settings.name; + if (currentPath.indexOf('/standard-page') == 0) { + widgetType = 'standard'; + } + Map params = { + 'type': widgetType, + "url": currentPath, + "name": widget.title + }; + DataUtils.checkCollected(params).then((result) { + if (this.mounted) { + setState(() { + _hasCollected = result ?? null; + }); } }); - if (this.mounted) { - setState(() { - _hasCollected = list.length > 0; - }); - } }); } // 点击收藏按钮 _getCollection() { + String currentRouterPath = ModalRoute.of(context).settings.name; + Map params = { + "type": widgetType, + "url": currentRouterPath, + "name": widget.title + }; if (_hasCollected) { // 删除操作 - _collectionControl.deleteByName(widget.title).then((result) { - if (result > 0 && this.mounted) { - setState(() { - _hasCollected = false; - }); + DataUtils.removeCollected(params, context).then((result) { + if (result) { _scaffoldKey.currentState .showSnackBar(SnackBar(content: Text('已取消收藏'))); if (ApplicationEvent.event != null) { ApplicationEvent.event - .fire(CollectionEvent(widget.title, _router, true)); + .fire(CollectionEvent(widget.title, currentRouterPath, true)); + } + if (this.mounted) { + setState(() { + _hasCollected = false; + }); } - return; } - print('删除错误'); }); } else { // 插入操作 - _collectionControl - .insert(Collection(name: widget.title, router: _router)) - .then((result) { - if (this.mounted) { - setState(() { - _hasCollected = true; - }); - - if (ApplicationEvent.event != null) { - ApplicationEvent.event - .fire(CollectionEvent(widget.title, _router, false)); + DataUtils.addCollected(params, context).then((result) { + if (result) { + if (this.mounted) { + setState(() { + _hasCollected = true; + }); } - _scaffoldKey.currentState .showSnackBar(SnackBar(content: Text('收藏成功'))); + if (ApplicationEvent.event != null) { + ApplicationEvent.event + .fire(CollectionEvent(widget.title, currentRouterPath, false)); + } } }); } @@ -129,7 +141,38 @@ class _WidgetDemoState extends State { '${Routes.codeView}?filePath=${Uri.encodeComponent(widget.codeUrl)}'); } } - + List> buildPopupMenu() { + List> comps = []; + if (widget.docUrl != null) { + comps.add( + PopupMenuItem( + value: 'doc', + child: ListTile( + leading: Icon( + Icons.library_books, + size: 22.0, + ), + title: Text('查看文档'), + ), + ) + ); + } + if (widget.codeUrl != null) { + comps.add( + PopupMenuItem( + value: 'code', + child: ListTile( + leading: Icon( + Icons.code, + size: 22.0, + ), + title: Text('查看Demo'), + ), + ) + ); + } + return comps; + } @override Widget build(BuildContext context) { if (_hasCollected) { @@ -137,50 +180,34 @@ class _WidgetDemoState extends State { } else { _collectionIcons = Icons.favorite_border; } + List> menus = buildPopupMenu(); + List actions = [ + new IconButton( + tooltip: 'goBack home', + onPressed: () { + Navigator.popUntil(context, ModalRoute.withName(Routes.root)); + }, + icon: Icon(Icons.home), + ), + new IconButton( + tooltip: 'collection', + onPressed: _getCollection, + icon: Icon(_collectionIcons), + ), + ]; + if (menus.length > 0) { + actions.add( + PopupMenuButton( + onSelected: _selectValue, + itemBuilder: (BuildContext context) => menus, + ) + ); + } return Scaffold( key: _scaffoldKey, appBar: AppBar( title: Text(widget.title), - actions: [ - new IconButton( - tooltip: 'goBack home', - onPressed: () { - Navigator.popUntil(context, ModalRoute.withName('/')); - }, - icon: Icon(Icons.home), - ), - new IconButton( - tooltip: 'collection', - onPressed: _getCollection, - icon: Icon(_collectionIcons), - ), - PopupMenuButton( - onSelected: _selectValue, - itemBuilder: (BuildContext context) => >[ - const PopupMenuItem( - value: 'doc', - child: ListTile( - leading: Icon( - Icons.library_books, - size: 22.0, - ), - title: Text('查看文档'), - ), - ), - const PopupMenuDivider(), - const PopupMenuItem( - value: 'code', - child: ListTile( - leading: Icon( - Icons.code, - size: 22.0, - ), - title: Text('查看Demo'), - ), - ), - ], - ), - ], + actions: actions, ), body: Container( padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 15.0), diff --git a/lib/components/widget_item_container.dart b/lib/components/widget_item_container.dart index a1461857..d7438790 100644 --- a/lib/components/widget_item_container.dart +++ b/lib/components/widget_item_container.dart @@ -3,56 +3,91 @@ import 'package:fluro/fluro.dart'; import './widget_item.dart'; import '../routers/application.dart'; import '../widgets/index.dart'; +import '../model/widget.dart'; class WidgetItemContainer extends StatelessWidget { final int columnCount; //一行几个 - final List categories; - final bool isWidgetPoint; + final List commonItems; +// final bool isWidgetPoint; // 所有的可用demos; final List widgetDemosList = new WidgetDemoList().getDemos(); WidgetItemContainer( {Key key, - @required this.categories, + @required this.commonItems, @required this.columnCount, - @required this.isWidgetPoint}) +// @required this.isWidgetPoint + }) : super(key: key); + /// 跳转goup + void tapToGroup(CategoryComponent cate, BuildContext context) { + Application.router + .navigateTo(context, "/category/${cate.token}", transition: TransitionType.inFromRight); + } + + /// 跳转到老的widget界面 + void tapToOldWidget(WidgetLeaf leaf, BuildContext context) { + + String targetName = leaf.name; + String targetRouter = '/category/error/404'; + widgetDemosList.forEach((item) { + if (item.name == targetName) { + targetRouter = item.routerName; + targetRouter = targetRouter.toLowerCase(); + } + }); + Application.router.navigateTo(context, targetRouter, transition: TransitionType.inFromRight); + } + + /// 跳转到新的标准页 + void tapToStandardPage(WidgetLeaf leaf, BuildContext context) { + String targetRouter = '/standard-page/${leaf.pageId}'; + Application.router.navigateTo(context, targetRouter, transition: TransitionType.inFromRight); + } + List _buildColumns(context) { List _listWidget = []; List _listRows = []; int addI; - for (int i = 0, length = categories.length; i < length; i += columnCount) { + for (int i = 0, length = commonItems.length; i < length; i += columnCount) { _listRows = []; for (int innerI = 0; innerI < columnCount; innerI++) { addI = innerI + i; if (addI < length) { - dynamic item = categories[addI]; + CommonItem item = commonItems[addI]; + + _listRows.add( Expanded( flex: 1, child: WidgetItem( title: item.name, onTap: () { - if (isWidgetPoint) { - String targetName = item.name; - String targetRouter = '/category/error/404'; - widgetDemosList.forEach((item) { - if (item.name == targetName) { - targetRouter = item.routerName; - } - }); - Application.router.navigateTo(context, "$targetRouter", transition: TransitionType.inFromRight); - } else { - Application.router - .navigateTo(context, "/category/${item.name}", transition: TransitionType.inFromRight); + String type = item.type; + + if (type == "category") { + return tapToGroup(item as CategoryComponent, context); } + if (type == "widget") { + WidgetLeaf leaf = item as WidgetLeaf; + + if (leaf.display == "standard") { + return tapToStandardPage(leaf, context); + } else { + return tapToOldWidget(leaf, context); + } + } + + Application.router + .navigateTo(context, "/category/error/404", transition: TransitionType.inFromRight); }, index: addI, totalCount: length, rowLength: columnCount, - textSize: isWidgetPoint ? 'middle' : 'small', + /// textSize: true ? 'middle' : 'small', + textSize: 'middle' ), ), ); @@ -81,3 +116,4 @@ class WidgetItemContainer extends StatelessWidget { ); } } + diff --git a/lib/event/event_bus.dart b/lib/event/event_bus.dart index ce2123df..d51e9b10 100644 --- a/lib/event/event_bus.dart +++ b/lib/event/event_bus.dart @@ -2,4 +2,4 @@ import 'package:event_bus/event_bus.dart'; class ApplicationEvent{ static EventBus event; -} \ No newline at end of file +} diff --git a/lib/event/event_model.dart b/lib/event/event_model.dart index f72f0cb6..ca753ec9 100644 --- a/lib/event/event_model.dart +++ b/lib/event/event_model.dart @@ -4,4 +4,16 @@ class CollectionEvent{ final bool isRemove; // token uid... CollectionEvent(this.widgetName,this.router,this.isRemove); +} + +class UserGithubOAuthEvent{ + final String loginName; + final String token; + final bool isSuccess; + UserGithubOAuthEvent(this.loginName,this.token,this.isSuccess); +} + +class UserSettingThemeColorEvent{ + final int settingThemeColor; + UserSettingThemeColorEvent(this.settingThemeColor); } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 89fd9d3d..ec88b683 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,8 @@ -import 'dart:async'; -import 'dart:core'; - import 'package:flutter/material.dart'; import 'package:fluro/fluro.dart'; import 'package:flutter/rendering.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'routers/routers.dart'; -import 'routers/application.dart'; +import 'routers/application.dart' show Application; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_go/utils/provider.dart'; import 'package:flutter_go/utils/shared_preferences.dart'; @@ -16,19 +12,24 @@ import 'package:flutter_go/model/search_history.dart'; import 'package:flutter_go/utils/analytics.dart' as Analytics; import 'package:flutter_go/views/login_page/login_page.dart'; import 'package:flutter_go/utils/data_utils.dart'; - +import 'package:flutter_go/model/user_info.dart'; +import 'package:flutter_jpush/flutter_jpush.dart'; +import 'package:flutter_go/event/event_bus.dart'; +import 'package:flutter_go/event/event_model.dart'; +import 'package:event_bus/event_bus.dart'; +import 'package:flutter_go/model/widget.dart'; +import 'package:flutter_go/standard_pages/index.dart'; //import 'views/welcome_page/index.dart'; +import 'package:flutter_go/utils/net_utils.dart'; -const int ThemeColor = 0xFFC91B3A; SpUtil sp; var db; class MyApp extends StatefulWidget { MyApp() { final router = new Router(); - Routes.configureRoutes(router); - + // 这里设置项目环境 Application.router = router; } @@ -39,70 +40,144 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { bool _hasLogin = false; bool _isLoading = true; + UserInformation _userInfo; + bool isConnected = false; + String registrationId; + List notificationList = []; + int themeColor = 0xFFC91B3A; + + _MyAppState() { + final eventBus = new EventBus(); + ApplicationEvent.event = eventBus; + } + + /// 服务端控制是否显示业界动态 + Future _reqsMainPageIsOpen() async { + const reqs = 'https://flutter-go.pub/api/isInfoOpen'; + var response; + try{ + response = await NetUtils.get(reqs, {}); + print('response-$response'); + if(response['status'] == 200 && response['success'] ==true && response['data'] is Map && response['data']['isOpen'] == true) { + Application.pageIsOpen = true; + print('是否需要展开【业界动态】${Application.pageIsOpen}'); + } + }catch(e){ + print('response-$e'); + } + return response; + } @override - Future initState() { + void initState() { super.initState(); - var platformAandroid = - (Theme.of(context).platform == TargetPlatform.android); - DataUtils.checkVersion({'name': 'FlutterGo'}).then((bool) { - print("返回值back ${bool}"); - if (platformAandroid && bool) { - setState(() {}); - _UpdateURL(); - } - }).catchError((onError) { - print('获取失败:$onError'); + _reqsMainPageIsOpen(); + _startupJpush(); + + FlutterJPush.addConnectionChangeListener((bool connected) { + setState(() { + /// 是否连接,连接了才可以推送 + print("连接状态改变:$connected"); + this.isConnected = connected; + if (connected) { + //在启动的时候会去连接自己的服务器,连接并注册成功之后会返回一个唯一的设备号 + try { + FlutterJPush.getRegistrationID().then((String regId) { + print("主动获取设备号:$regId"); + setState(() { + this.registrationId = regId; + }); + }); + } catch (error) { + print('主动获取设备号Error:$error'); + } + } + }); + }); + + FlutterJPush.addReceiveNotificationListener( + (JPushNotification notification) { + setState(() { + /// 收到推送 + print("收到推送提醒: $notification"); + notificationList.add(notification); + }); + }); + + FlutterJPush.addReceiveOpenNotificationListener( + (JPushNotification notification) { + setState(() { + print("打开了推送提醒: $notification"); + + /// 打开了推送提醒 + notificationList.add(notification); + }); + }); + + FlutterJPush.addReceiveCustomMsgListener((JPushMessage msg) { + setState(() { + print("收到推送消息提醒: $msg"); + + /// 打开了推送提醒 + notificationList.add(msg); + }); }); DataUtils.checkLogin().then((hasLogin) { - setState(() { - _hasLogin = hasLogin; - _isLoading = false; - }); + if (hasLogin.runtimeType == UserInformation) { + setState(() { + _hasLogin = true; + _isLoading = false; + _userInfo = hasLogin; + // 设置初始化的主题色 + // if (hasLogin.themeColor != 'default') { + // themeColor = int.parse(hasLogin.themeColor); + // } + }); + } else { + setState(() { + _hasLogin = hasLogin; + _isLoading = false; + }); + } }).catchError((onError) { setState(() { - _hasLogin = true; + _hasLogin = false; _isLoading = false; }); print('身份信息验证失败:$onError'); }); - } - _UpdateURL() async { - const currUrl = - 'https://github.com/alibaba/flutter-go/raw/master/FlutterGo.apk'; - if (await canLaunch(currUrl)) { - await launch(currUrl); - } else { - throw 'Could not launch $currUrl'; - } + ApplicationEvent.event.on().listen((event) { + print('接收到的 event $event'); + }); } showWelcomePage() { -// if (_isLoading) { -// return Container( -// color: const Color(ThemeColor), -// child: Center( -// child: SpinKitPouringHourglass(color: Colors.white), -// ), -// ); -// } else { -// // 判断是否已经登录 -// if (_hasLogin) { - return AppPage(); -// } else { -// return LoginPage(); -// } -// } + if (_isLoading) { + return Container( + color: Color(this.themeColor), + child: Center( + child: SpinKitPouringHourglass(color: Colors.white), + ), + ); + } else { + // 判断是否已经登录 + if (_hasLogin) { + return AppPage(_userInfo); + } else { + return LoginPage(); + } + } } @override Widget build(BuildContext context) { +// WidgetTree.getCommonItemByPath([15, 17], Application.widgetTree); return new MaterialApp( - title: 'title', + title: 'titles', theme: new ThemeData( - primaryColor: Color(ThemeColor), + primaryColor: Color(this.themeColor), backgroundColor: Color(0xFFEFEFEF), accentColor: Color(0xFF888888), textTheme: TextTheme( @@ -110,14 +185,18 @@ class _MyAppState extends State { body1: TextStyle(color: Color(0xFF888888), fontSize: 16.0), ), iconTheme: IconThemeData( - color: Color(ThemeColor), + color: Color(this.themeColor), size: 35.0, ), ), +<<<<<<< HEAD home: new Scaffold( body: showWelcomePage() ), +======= + home: new Scaffold(body: showWelcomePage()), +>>>>>>> dxj/master debugShowCheckedModeBanner: false, onGenerateRoute: Application.router.generator, navigatorObservers: [Analytics.observer], @@ -125,11 +204,25 @@ class _MyAppState extends State { } } +void _startupJpush() async { + print("初始化jpush"); + await FlutterJPush.startup(); + print("初始化jpush成功"); +} + void main() async { + WidgetsFlutterBinding.ensureInitialized(); final provider = new Provider(); await provider.init(true); sp = await SpUtil.getInstance(); new SearchHistoryList(sp); + + await DataUtils.getWidgetTreeList().then((List json) { + List data = WidgetTree.insertDevPagesToList(json, StandardPages().getLocalList()); + Application.widgetTree = WidgetTree.buildWidgetTree(data); + print("Application.widgetTree>>>> ${Application.widgetTree}"); + }); db = Provider.db; runApp(new MyApp()); } + diff --git a/lib/model/cat.dart b/lib/model/cat.dart index e9ecae7b..5ac493f4 100644 --- a/lib/model/cat.dart +++ b/lib/model/cat.dart @@ -1,107 +1,107 @@ - -import 'dart:async'; - -import 'package:flutter_go/utils/sql.dart'; - -abstract class CatInterface{ - int get id; - //类目名称 - String get name; - //描述 - String get desc; - //第几级类目,默认 1 - int get depth; - //父类目id,没有为 0 - int get parentId; -} - -class Cat implements CatInterface { - int id; - String name; - String desc; - int depth; - int parentId; - - Cat({this.id, this.name, this.desc, this.depth, this.parentId}); - - Cat.fromJSON(Map json) - : id = json['id'], - name = json['name'], - desc = json['desc'], - depth = json['depth'], - parentId = json['parentId']; - - String toString() { - return '(Cat $name)'; - } - - Map toMap() { - return { - 'id': id, - 'name': name, - 'desc': desc, - 'depth': depth, - 'parentId': parentId - }; - } - Map toSqlCondition() { - Map _map = this.toMap(); - Map condition = {}; - _map.forEach((k, value) { - - if (value != null) { - - condition[k] = value; - } - }); - - if (condition.isEmpty) { - return {}; - } - - return condition; - } -} - - -class CatControlModel{ - final String table = 'cat'; - Sql sql; - CatControlModel() { - sql = Sql.setTable(table); - } - - /// 获取一级类目 - Future mainList() async{ - List listJson = await sql.getByCondition(conditions: {'parentId': 0}); - List cats = listJson.map((json) { - return new Cat.fromJSON(json); - }).toList(); - return cats; - } - - // 获取Cat不同深度与parent的列表 - Future> getList([Cat cat]) async{ - - - if (cat == null) { - cat = new Cat(depth: 1, parentId: 0); - } - // print("cat in getList ${cat.toMap()}"); - List listJson = await sql.getByCondition(conditions: cat.toSqlCondition()); - List cats = listJson.map((json) { - return new Cat.fromJSON(json); - }).toList(); - return cats; - } - - // 通过name获取Cat对象信息 - Future getCatByName(String name) async { - List json = await sql.getByCondition(conditions: {'name': name}); - if (json.isEmpty) { - return null; - } - return new Cat.fromJSON(json.first); - } - -} +// +//import 'dart:async'; +// +//import 'package:flutter_go/utils/sql.dart'; +// +//abstract class CatInterface{ +// int get id; +// //类目名称 +// String get name; +// //描述 +// String get desc; +// //第几级类目,默认 1 +// int get depth; +// //父类目id,没有为 0 +// int get parentId; +//} +// +//class Cat implements CatInterface { +// int id; +// String name; +// String desc; +// int depth; +// int parentId; +// +// Cat({this.id, this.name, this.desc, this.depth, this.parentId}); +// +// Cat.fromJSON(Map json) +// : id = json['id'], +// name = json['name'], +// desc = json['desc'], +// depth = json['depth'], +// parentId = json['parentId']; +// +// String toString() { +// return '(Cat $name)'; +// } +// +// Map toMap() { +// return { +// 'id': id, +// 'name': name, +// 'desc': desc, +// 'depth': depth, +// 'parentId': parentId +// }; +// } +// Map toSqlCondition() { +// Map _map = this.toMap(); +// Map condition = {}; +// _map.forEach((k, value) { +// +// if (value != null) { +// +// condition[k] = value; +// } +// }); +// +// if (condition.isEmpty) { +// return {}; +// } +// +// return condition; +// } +//} +// +// +//class CatControlModel{ +// final String table = 'cat'; +// Sql sql; +// CatControlModel() { +// sql = Sql.setTable(table); +// } +// +// /// 获取一级类目 +// Future mainList() async{ +// List listJson = await sql.getByCondition(conditions: {'parentId': 0}); +// List cats = listJson.map((json) { +// return new Cat.fromJSON(json); +// }).toList(); +// return cats; +// } +// +// // 获取Cat不同深度与parent的列表 +// Future> getList([Cat cat]) async{ +// +// +// if (cat == null) { +// cat = new Cat(depth: 1, parentId: 0); +// } +// // print("cat in getList ${cat.toMap()}"); +// List listJson = await sql.getByCondition(conditions: cat.toSqlCondition()); +// List cats = listJson.map((json) { +// return new Cat.fromJSON(json); +// }).toList(); +// return cats; +// } +// +// // 通过name获取Cat对象信息 +// Future getCatByName(String name) async { +// List json = await sql.getByCondition(conditions: {'name': name}); +// if (json.isEmpty) { +// return null; +// } +// return new Cat.fromJSON(json.first); +// } +// +//} diff --git a/lib/model/collection.dart b/lib/model/collection.dart index 388fb3d8..49233bbe 100644 --- a/lib/model/collection.dart +++ b/lib/model/collection.dart @@ -49,15 +49,21 @@ class CollectionControlModel { List list = await sql.getByCondition(); List resultList = []; list.forEach((item){ - print(item); + print('collection item =>> $item'); resultList.add(Collection.fromJSON(item)); }); return resultList; } - // 通过收藏名获取router - Future getRouterByName(String name) async { - List list = await sql.getByCondition(conditions: {'name': name}); + /// 通过收藏名获取router + /// 因为名称很容易重复. 所以这里使用path router做唯一判断 +// Future getRouterByName(String name) async { +// List list = await sql.getByCondition(conditions: {'name': name}); +// return list; +// } + + Future getRouterByUrl(String path) async { + List list = await sql.getByCondition(conditions: {'router': path}); return list; } @@ -65,4 +71,8 @@ class CollectionControlModel { Future deleteByName(String name) async{ return await sql.delete(name,'name'); } + // 通过path删除 + Future deleteByPath(String path) async{ + return await sql.delete(path,'router'); + } } diff --git a/lib/model/responseData.dart b/lib/model/responseData.dart new file mode 100644 index 00000000..1b5327e5 --- /dev/null +++ b/lib/model/responseData.dart @@ -0,0 +1,25 @@ +class ResponseData{ + int status; + bool success; + String message; + + ResponseData(this.status, this.success,this.message); + + ResponseData.fromJson(Map json) + : status = json['status'], + success = json['success'], + message=json['message']; + + Map toJson() => + { + 'status': status, + 'success': success, + 'messsage': message + }; + + @override + String toString() { + return 'status: $status ,success: $success,message: ${message.toString()}'; + } + +} \ No newline at end of file diff --git a/lib/model/search_history.dart b/lib/model/search_history.dart index da2e4cdd..ecaef036 100644 --- a/lib/model/search_history.dart +++ b/lib/model/search_history.dart @@ -73,7 +73,7 @@ class SearchHistoryList { } add(SearchHistory item) { - print("_searchHistoryList> ${_searchHistoryList.length}"); + print("add item to serach history ${item.targetRouter}"); for (SearchHistory value in _searchHistoryList) { if (value.name == item.name) { return; diff --git a/lib/model/user_info.dart b/lib/model/user_info.dart index b47b08a3..5007e512 100644 --- a/lib/model/user_info.dart +++ b/lib/model/user_info.dart @@ -1,24 +1,32 @@ -class UserInfo { +class UserInformation { String username; int id; String avatarPic; String themeColor; - String urlName; - UserInfo({ + UserInformation({ this.avatarPic, this.id, this.themeColor, - this.urlName, this.username, }); - factory UserInfo.fromJson(Map json) { - return UserInfo( + factory UserInformation.fromJson(Map json) { + print('fromJOSN $json ${json['id'].runtimeType}'); + String name = json['name']; + int userId ; + if(json['name'] == null){ + name = json['url_name']; + } + if(json['id'].runtimeType == int){ + userId = json['id']; + }else{ + userId = int.parse(json['id']); + } + return UserInformation( avatarPic: json['avatar_pic'], - id: int.parse(json['id']), - username: json['name'], - themeColor: json['theme_color'], - urlName: json['url_name']); + id: userId, + username: name, + themeColor: json['theme_color']); } } diff --git a/lib/model/widget.dart b/lib/model/widget.dart index bcc6ae7f..c0f37d0d 100644 --- a/lib/model/widget.dart +++ b/lib/model/widget.dart @@ -2,9 +2,16 @@ import 'dart:async'; import "package:flutter/material.dart"; - +import "package:flutter_go/routers/application.dart"; import 'package:flutter_go/utils/sql.dart'; +enum treeNode { + CategoryComponent, + WidgetLeaf +} + +//typedef aaa + abstract class WidgetInterface { int get id; @@ -142,3 +149,226 @@ class WidgetControlModel { return widgets; } } +// 抽象类 +abstract class CommonItem { + int id; + String name; + int parentId; + String type; + List children; + String token; + + /// 父级节点, 存放整个CommonItem对象node = ! null + /// + CommonItem parent; + String toString() { + return "CommonItem {name: $name, type: $type, parentId: $parentId, token: $token, children长度 $children"; + } + + T getChild(String token); + T addChildren(Object item); + // 从children树中. 查找任意子节点 + T find(String token, [CommonItem node]); +} + +// tree的group树干 +class CategoryComponent extends CommonItem { + int id; + String name; + int parentId; + CommonItem parent; + String token; + + + List children = []; + + String type = 'category'; + + CategoryComponent({ + @required this.id, + @required this.name, + @required this.parentId, + this.type = 'categoryw', + this.children, + this.parent + }); + CategoryComponent.fromJson(Map json) { + if (json['id'] != null && json['id'].runtimeType == String) { + this.id = int.parse(json['id']); + } else { + this.id = json['id']; + } + this.name = json['name']; + this.parentId = json['parentId']; + this.token = json['id'].toString() + json['type']; + } + void addChildren(Object item) { + if (item is CategoryComponent) { + CategoryComponent cate = item; + cate.parent = this; + this.children.add( + cate + ); + } + if (item is WidgetLeaf) { + WidgetLeaf widget = item; + widget.parent = this; + this.children.add( + widget + ); + } + } + @override + CommonItem getChild(String token) { + return children.firstWhere((CommonItem item) => item.token == token, orElse: () => null); + } + + @override + CommonItem find(String token, [CommonItem node]) { + CommonItem ret; + if (node !=null) { + if (node.token == token) { + return node; + } else { + // 循环到叶子节点, 返回 空 + if (node.children == null) { + return null; + } + for (int i = 0; i < node.children.length; i++) { + CommonItem temp = this.find(token, node.children[i]); + if (temp != null) { + ret = temp; + } + } + } + } else { + ret = find(token, this); + } + return ret; + } +} + +// 叶子节点 +class WidgetLeaf extends CommonItem { + int id; + String name; + int parentId; + String display; // 展示类型, 区分老的widget文件下的详情 + String author; // 文档负责人 + String path; // 路由地址 + String pageId; // 界面ID + CommonItem parent; + + String type = 'widget'; + WidgetLeaf({ + @required this.id, + @required this.name, + @required this.display, + this.author, + this.path, + this.pageId + }); + + WidgetLeaf.fromJson(Map json) { + if (json['id'] != null && json['id'].runtimeType == String) { + this.id = int.parse(json['id']); + } else { + this.id = json['id']; + } + this.name = json['name']; + this.display = json['display']; + this.author = json['author'] ?? null; + this.path = json['path'] ?? null; + this.pageId = json['pageId'] ?? null; + this.token = json['id'].toString() + json['type']; + } + @override + CommonItem getChild(String token) { + return null; + } + @override + addChildren(Object item) { + // TODO: implement addChildren + return null; + } + + CommonItem find(String token, [CommonItem node]){ + return null; + } +} + +class WidgetTree { + // 构建树型结构 + static CategoryComponent buildWidgetTree(List json, [parent]){ + CategoryComponent current; + if (parent != null) { + current = parent; + } else { + current = CategoryComponent(id: 0, name: 'root', parentId: null, children: []); + } + json.forEach((item) { + // 归属分类级别 + if (['root', 'category'].indexOf(item['type']) != -1) { + CategoryComponent cate = CategoryComponent.fromJson(item); + if (cate.children != null) { + buildWidgetTree(item['children'], cate); + } + current.addChildren(cate); + } else { + // 归属最后一层叶子节点 + WidgetLeaf cate = WidgetLeaf.fromJson(item); + current.addChildren(cate); + } + }); + return current; + } + + static insertDevPagesToList(List list, List devPages) { + List devChildren = []; + int index = 9999999; + if (Application.env == ENV.PRODUCTION) { + return list; + } + devPages.forEach((item) { + index++; + if (item['id'] != null) { + devChildren.add({ + "id": index.toString(), + "name": item['name'], + "parentId": "99999999999", + "type": "widget", + "display": "standard", + "author": item['author'], + "pageId": item['id'] + }); + } + }); + list.forEach((item) { + if (item['name'].toString().toUpperCase() == 'DEVELOPER') { + List children = item['children']; + children.insert(0, { + "id": "99999999999", + "name": "本地代码", + "parentId": item['id'], + "type": "category", + "children": devChildren + }); + } + }); + return list; + } + static CategoryComponent getCommonItemById(List path, CategoryComponent root) { + print("getCommonItemByPath $path"); + print("root $root"); + CommonItem childLeaf; + /// int first = path.first; + path = path.sublist(1); + print("path:::: $path"); + if (path.length >= 0) { +// childLeaf = root.getChild(path.first); + } + + + return childLeaf; + } +} \ No newline at end of file diff --git a/lib/page_demo_package/.demo.json b/lib/page_demo_package/.demo.json new file mode 100644 index 00000000..ff076189 --- /dev/null +++ b/lib/page_demo_package/.demo.json @@ -0,0 +1 @@ +[{"name":"PullToRefresh","screenShot":"","author":"chenfeihu","email":"763551832@qq.com","desc":"刷新组件","id":"5553db80_52ae_4241_9c8a_5c9e1f92b096"},{"name":"RangeSlider","screenShot":"","author":"RangeSlider","email":"hanxu317@qq.com","desc":"RangeSlider widget demo","id":"e5f958bc_52ae_4241_9c8a_5c9e1f92b096"},{"name":"demoName","screenShot":"","author":"yourName","email":"yourEmail","desc":"这是一个测试的标准demo","id":"1a29aa8e_32ae_4241_9c8a_5c9e1f92b096"},{"name":"local","screenShot":"","author":"ab","email":"email","desc":"ags","id":"2c1d57d0_42ae_4241_9c8a_5c9e1f92b096"}] \ No newline at end of file diff --git a/lib/page_demo_package/PullToRefresh_chenfeihu_5553db80_52ae_4241_9c8a_5c9e1f92b096/.demo.json b/lib/page_demo_package/PullToRefresh_chenfeihu_5553db80_52ae_4241_9c8a_5c9e1f92b096/.demo.json new file mode 100644 index 00000000..1fe09bd0 --- /dev/null +++ b/lib/page_demo_package/PullToRefresh_chenfeihu_5553db80_52ae_4241_9c8a_5c9e1f92b096/.demo.json @@ -0,0 +1,9 @@ +{ + "name": "PullToRefresh", + "screenShot": "", + "author":"chenfeihu", + "email": "763551832@qq.com", + "desc": "刷新组件", + "id": "5553db80_52ae_4241_9c8a_5c9e1f92b096" +} + \ No newline at end of file diff --git a/lib/page_demo_package/PullToRefresh_chenfeihu_5553db80_52ae_4241_9c8a_5c9e1f92b096/index.dart b/lib/page_demo_package/PullToRefresh_chenfeihu_5553db80_52ae_4241_9c8a_5c9e1f92b096/index.dart new file mode 100644 index 00000000..f3f84726 --- /dev/null +++ b/lib/page_demo_package/PullToRefresh_chenfeihu_5553db80_52ae_4241_9c8a_5c9e1f92b096/index.dart @@ -0,0 +1,15 @@ +// +// Created with flutter go cli +// User: chenfeihu +// Time: 2019-09-24 12:11:14.085126 +// email: 763551832@qq.com +// desc: 刷新组件 +// + +import 'src/index.dart'; + +var demoWidgets = [ + new Demo() +]; + + \ No newline at end of file diff --git a/lib/page_demo_package/PullToRefresh_chenfeihu_5553db80_52ae_4241_9c8a_5c9e1f92b096/src/index.dart b/lib/page_demo_package/PullToRefresh_chenfeihu_5553db80_52ae_4241_9c8a_5c9e1f92b096/src/index.dart new file mode 100644 index 00000000..1558967c --- /dev/null +++ b/lib/page_demo_package/PullToRefresh_chenfeihu_5553db80_52ae_4241_9c8a_5c9e1f92b096/src/index.dart @@ -0,0 +1,241 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; + + +class Demo extends StatefulWidget { + @override + _State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.all(5.0), + width: double.infinity, + height: 600.0, + child: Pulltorefresh(), + ); + } +} + +class Pulltorefresh extends StatefulWidget { + @override + _PulltorefreshState createState() => _PulltorefreshState(); +} + +class _PulltorefreshState extends State { + List datas=ListData.getList(); + RefreshController _controller=RefreshController(initialRefresh: false); + + void _onRefresh() async{ + await Future.delayed(Duration(milliseconds: 1000)); + _controller.refreshCompleted(); + } + + void _onLoading() async{ + await Future.delayed(Duration(milliseconds: 1500)); + ItemModel model=ItemModel(getRandomColor(), Icons.airplanemode_active, "军事新闻", "俄军大秀战略", + "酝酿已久的俄罗斯“中部-2019”战略演习于16日正式启动", 5000); + + this.datas.add(Item(getRandomColor(), model.icon, model.mainTitle, model.subTitle, model.des, model.readCount)); + this.datas.add(Item(getRandomColor(), model.icon, model.mainTitle, model.subTitle, model.des, model.readCount)); + this.datas.add(Item(getRandomColor(), model.icon, model.mainTitle, model.subTitle, model.des, model.readCount)); + this.datas.add(Item(getRandomColor(), model.icon, model.mainTitle, model.subTitle, model.des, model.readCount)); + this.datas.add(Item(getRandomColor(), model.icon, model.mainTitle, model.subTitle, model.des, model.readCount)); + this.datas.add(Item(getRandomColor(), model.icon, model.mainTitle, model.subTitle, model.des, model.readCount)); + this.datas.add(Item(getRandomColor(), model.icon, model.mainTitle, model.subTitle, model.des, model.readCount)); + this.datas.add(Item(getRandomColor(), model.icon, model.mainTitle, model.subTitle, model.des, model.readCount)); + if(mounted) + setState(() { + + }); + _controller.loadComplete(); + } + + @override + Widget build(BuildContext context) { + + Widget _itemBuilder(BuildContext context, int position) { + + return Card(child: this.datas[position]); + + } + return Scaffold( + appBar: AppBar( + title: Text("Pulltorefresh"), + ), + body: SmartRefresher( + enablePullDown: true, + enablePullUp: true, + header: WaterDropHeader(), + footer: ClassicFooter( + loadStyle: LoadStyle.ShowAlways, + completeDuration: Duration(microseconds: 50), + ), + onRefresh: _onRefresh, + onLoading: _onLoading, + controller: _controller, + child: ListView.builder(itemBuilder: _itemBuilder,itemCount: this.datas.length), + + + ), + + + ); + } + + + Color getRandomColor(){ + List colors=[Colors.deepOrange,Colors.amber,Colors.cyan,Colors.green,Colors.red,Colors.yellow]; + int randomValue=Random().nextInt(colors.length); + if(randomValue==colors.length){ + randomValue=colors.length-1; + } + return colors[randomValue]; + + } + +} + + + +class Item extends StatelessWidget { + Color color; + IconData icon; + String mainTitle; + String subTitle; + String des; + int readCount; + + Item(this.color, this.icon, this.mainTitle, this.subTitle, this.des, + this.readCount); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.fromLTRB(10.0, 5.0, 10.0, 5.0), + height: 90.0, + child: Row( + children: [ + Container( + width: 90.0, + color: color, + alignment: Alignment.center, + child: Icon(icon, color: Colors.white), + ), + SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text(mainTitle, + style: TextStyle( + fontWeight: FontWeight.bold, fontSize: 18.0))), + Expanded(child: Text(subTitle, style: TextStyle(fontSize: 14.0))), + Expanded( + child: Text( + des, + style: TextStyle(fontSize: 13.0), + overflow: TextOverflow.ellipsis, + )), + Expanded( + child: Text("阅读量:${readCount.toString()}", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14.0, + color: Colors.redAccent))), + ], + )) + ], + ), + ); + } +} + +class ItemModel{ + Color _color; + IconData _icon; + String _mainTitle; + String _subTitle; + String _des; + int _readCount; + + ItemModel(this._color, this._icon, this._mainTitle, this._subTitle, this._des, + this._readCount); + + int get readCount => _readCount; + + set readCount(int value) { + _readCount = value; + } + + String get des => _des; + + set des(String value) { + _des = value; + } + + String get subTitle => _subTitle; + + set subTitle(String value) { + _subTitle = value; + } + + String get mainTitle => _mainTitle; + + set mainTitle(String value) { + _mainTitle = value; + } + + IconData get icon => _icon; + + set icon(IconData value) { + _icon = value; + } + + Color get color => _color; + + set color(Color value) { + _color = value; + } + + +} + +class ListData{ + static List getList(){ + List models=[]; + ItemModel model1= ItemModel(Colors.red, Icons.airplanemode_active, "军事新闻", "俄军大秀战略", + "酝酿已久的俄罗斯“中部-2019”战略演习于16日正式启动", 2999); + ItemModel model2= ItemModel(Colors.red, Icons.airplanemode_active, "军事新闻", "俄“中部”演习", + "俄罗斯卫星网报道称,俄罗斯国防部长绍伊古表示,“中央-2019”战略演习是", 4588); + ItemModel model3= ItemModel(Colors.red, Icons.airplanemode_active, "军事新闻", "中国2.7万吨坞登舰", + "据印度新德里电视台16日报道,印度海军发现7艘中国军舰在印度洋", 7777); + ItemModel model4= ItemModel(Colors.red, Icons.airplanemode_active, "军事新闻", "针对中国?", + "美国空军着力打造军用5G网络,5G+VR,飞行员无需上天就能操控战机;美军濒海", 8888); + ItemModel model5= ItemModel(Colors.red, Icons.airplanemode_active, "军事新闻", "“凯旋”防空导弹系统", + "俄罗斯卫星通讯社报道,俄罗斯北方舰队(Russian Northern Fleet)新闻处", 9999); + ItemModel model6= ItemModel(Colors.red, Icons.airplanemode_active, "军事新闻", "火箭军还有骑兵连", + "迅速对禁区“敌特分子”活动区域进行侦察定位,战斗小分队", 104754); + ItemModel model7= ItemModel(Colors.red, Icons.airplanemode_active, "军事新闻", "侦察兵跨越冰川", + "在海拔5000多米的雪域高原,第77集团军某合成旅的侦察兵们正在进行野外驻训", 47545); + ItemModel model8= ItemModel(Colors.red, Icons.airplanemode_active, "军事新闻", "香港被护商船", + "新京报快讯 据北海舰队官微消息:“感谢海军!”“祖国万岁!”,当地时", 124574); + + models.add(Item(model1.color, model1.icon, model1.mainTitle, model1.subTitle, model1.des, model1.readCount)); + models.add(Item(model2.color, model2.icon, model2.mainTitle, model2.subTitle, model2.des, model2.readCount)); + models.add(Item(model3.color, model3.icon, model3.mainTitle, model3.subTitle, model3.des, model3.readCount)); + models.add(Item(model4.color, model4.icon, model4.mainTitle, model4.subTitle, model4.des, model4.readCount)); + models.add(Item(model5.color, model5.icon, model5.mainTitle, model5.subTitle, model5.des, model5.readCount)); + models.add(Item(model6.color, model6.icon, model6.mainTitle, model6.subTitle, model6.des, model6.readCount)); + models.add(Item(model7.color, model7.icon, model7.mainTitle, model7.subTitle, model7.des, model7.readCount)); + models.add(Item(model8.color, model8.icon, model8.mainTitle, model8.subTitle, model8.des, model8.readCount)); + return models; + } +} + + diff --git a/lib/page_demo_package/RangeSlider_RangeSlider_e5f958bc_52ae_4241_9c8a_5c9e1f92b096/.demo.json b/lib/page_demo_package/RangeSlider_RangeSlider_e5f958bc_52ae_4241_9c8a_5c9e1f92b096/.demo.json new file mode 100644 index 00000000..10444611 --- /dev/null +++ b/lib/page_demo_package/RangeSlider_RangeSlider_e5f958bc_52ae_4241_9c8a_5c9e1f92b096/.demo.json @@ -0,0 +1,8 @@ +{ + "name": "RangeSlider", + "screenShot": "", + "author":"RangeSlider", + "email": "hanxu317@qq.com", + "desc": "RangeSlider widget demo", + "id": "e5f958bc_52ae_4241_9c8a_5c9e1f92b096" +} diff --git a/lib/page_demo_package/RangeSlider_RangeSlider_e5f958bc_52ae_4241_9c8a_5c9e1f92b096/index.dart b/lib/page_demo_package/RangeSlider_RangeSlider_e5f958bc_52ae_4241_9c8a_5c9e1f92b096/index.dart new file mode 100644 index 00000000..53760649 --- /dev/null +++ b/lib/page_demo_package/RangeSlider_RangeSlider_e5f958bc_52ae_4241_9c8a_5c9e1f92b096/index.dart @@ -0,0 +1,15 @@ +// +// Created with flutter go cli +// User: RangeSlider +// Time: 2019-09-12 15:11:05.512158 +// email: hanxu317@qq.com +// desc: RangeSlider widget demo +// + +import 'src/index.dart'; + +var demoWidgets = [ + new Demo() +]; + + \ No newline at end of file diff --git a/lib/page_demo_package/RangeSlider_RangeSlider_e5f958bc_52ae_4241_9c8a_5c9e1f92b096/src/index.dart b/lib/page_demo_package/RangeSlider_RangeSlider_e5f958bc_52ae_4241_9c8a_5c9e1f92b096/src/index.dart new file mode 100644 index 00000000..bc34bf8f --- /dev/null +++ b/lib/page_demo_package/RangeSlider_RangeSlider_e5f958bc_52ae_4241_9c8a_5c9e1f92b096/src/index.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class Demo extends StatefulWidget { + @override + _State createState() => _State(); +} + +class _State extends State { + RangeValues valuess = RangeValues(20.0, 50.0); + @override + Widget build(BuildContext context) { + return RangeSlider( + values: valuess, + //实际进度的位置 + inactiveColor: Colors.black12, + //进度中不活动部分的颜色 + labels: RangeLabels('12', '23'), + min: 0.0, + max: 100.0, + divisions: 1000, + activeColor: Colors.blue, + //进度中活动部分的颜色 + onChanged: (rangeValues) { + setState(() { + valuess = rangeValues; + }); + }, + ); + } +} diff --git a/lib/page_demo_package/demoName_yourName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096/.demo.json b/lib/page_demo_package/demoName_yourName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096/.demo.json new file mode 100644 index 00000000..de6b8e00 --- /dev/null +++ b/lib/page_demo_package/demoName_yourName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096/.demo.json @@ -0,0 +1,9 @@ +{ + "name": "demoName", + "screenShot": "", + "author":"yourName", + "email": "yourEmail", + "desc": "这是一个测试的标准demo", + "id": "1a29aa8e_32ae_4241_9c8a_5c9e1f92b096" +} + \ No newline at end of file diff --git a/lib/page_demo_package/demoName_yourName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096/index.dart b/lib/page_demo_package/demoName_yourName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096/index.dart new file mode 100644 index 00000000..68193686 --- /dev/null +++ b/lib/page_demo_package/demoName_yourName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096/index.dart @@ -0,0 +1,15 @@ +// +// Created with flutter go cli +// User: yourName +// Time: 2019-06-10 20:37:27.289097 +// email: yourEmail +// desc: 这是一个测试的标准demo +// + +import 'src/index.dart'; + +var demoWidgets = [ + new Demo() +]; + + \ No newline at end of file diff --git a/lib/page_demo_package/demoName_yourName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096/src/index.dart b/lib/page_demo_package/demoName_yourName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096/src/index.dart new file mode 100644 index 00000000..e2992379 --- /dev/null +++ b/lib/page_demo_package/demoName_yourName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096/src/index.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class Demo extends StatefulWidget { + @override + _State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return Container( + child: RaisedButton(onPressed: () {}, child: Text('我是md中引入的demo')) + ); + } +} + \ No newline at end of file diff --git a/lib/page_demo_package/index.dart b/lib/page_demo_package/index.dart new file mode 100644 index 00000000..c3ed991d --- /dev/null +++ b/lib/page_demo_package/index.dart @@ -0,0 +1,10 @@ +import 'PullToRefresh_chenfeihu_5553db80_52ae_4241_9c8a_5c9e1f92b096/index.dart' as StandardDemo_PullToRefresh_5553db80_52ae_4241_9c8a_5c9e1f92b096; +import 'RangeSlider_RangeSlider_e5f958bc_52ae_4241_9c8a_5c9e1f92b096/index.dart' as StandardDemo_RangeSlider_e5f958bc_52ae_4241_9c8a_5c9e1f92b096; +import 'demoName_yourName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096/index.dart' as StandardDemo_demoName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096; +import 'local_ab_2c1d57d0_42ae_4241_9c8a_5c9e1f92b096/index.dart' as StandardDemo_local_2c1d57d0_42ae_4241_9c8a_5c9e1f92b096; +var demoObjects = { + '5553db80_52ae_4241_9c8a_5c9e1f92b096': StandardDemo_PullToRefresh_5553db80_52ae_4241_9c8a_5c9e1f92b096.demoWidgets, + 'e5f958bc_52ae_4241_9c8a_5c9e1f92b096': StandardDemo_RangeSlider_e5f958bc_52ae_4241_9c8a_5c9e1f92b096.demoWidgets, + '1a29aa8e_32ae_4241_9c8a_5c9e1f92b096': StandardDemo_demoName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096.demoWidgets, + '2c1d57d0_42ae_4241_9c8a_5c9e1f92b096': StandardDemo_local_2c1d57d0_42ae_4241_9c8a_5c9e1f92b096.demoWidgets +}; \ No newline at end of file diff --git a/lib/page_demo_package/info.json b/lib/page_demo_package/info.json new file mode 100644 index 00000000..669b078a --- /dev/null +++ b/lib/page_demo_package/info.json @@ -0,0 +1,17 @@ +{ + "70c429df-c27d-4843-8e28-1e6885c9276b": { + "name": "button-red", + "screenShot": "", + "author": "sanfan", + "email": "sanfan.hx@alibaba-inc.com", + "desc": "desc", + } +} + + + + + + + + diff --git a/lib/page_demo_package/local_ab_2c1d57d0_42ae_4241_9c8a_5c9e1f92b096/.demo.json b/lib/page_demo_package/local_ab_2c1d57d0_42ae_4241_9c8a_5c9e1f92b096/.demo.json new file mode 100644 index 00000000..e6e8114d --- /dev/null +++ b/lib/page_demo_package/local_ab_2c1d57d0_42ae_4241_9c8a_5c9e1f92b096/.demo.json @@ -0,0 +1,9 @@ +{ + "name": "local", + "screenShot": "", + "author":"ab", + "email": "email", + "desc": "ags", + "id": "2c1d57d0_42ae_4241_9c8a_5c9e1f92b096" +} + \ No newline at end of file diff --git a/lib/page_demo_package/local_ab_2c1d57d0_42ae_4241_9c8a_5c9e1f92b096/index.dart b/lib/page_demo_package/local_ab_2c1d57d0_42ae_4241_9c8a_5c9e1f92b096/index.dart new file mode 100644 index 00000000..4e315373 --- /dev/null +++ b/lib/page_demo_package/local_ab_2c1d57d0_42ae_4241_9c8a_5c9e1f92b096/index.dart @@ -0,0 +1,15 @@ +// +// Created with flutter go cli +// User: ab +// Time: 2019-08-06 17:26:02.905889 +// email: email +// desc: ags +// + +import 'src/index.dart'; + +var demoWidgets = [ + new Demo() +]; + + \ No newline at end of file diff --git a/lib/page_demo_package/local_ab_2c1d57d0_42ae_4241_9c8a_5c9e1f92b096/src/index.dart b/lib/page_demo_package/local_ab_2c1d57d0_42ae_4241_9c8a_5c9e1f92b096/src/index.dart new file mode 100644 index 00000000..d0d3921e --- /dev/null +++ b/lib/page_demo_package/local_ab_2c1d57d0_42ae_4241_9c8a_5c9e1f92b096/src/index.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class Demo extends StatefulWidget { + @override + _State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return Container( + child: Text("this is flutter go init demo"), + ); + } +} + \ No newline at end of file diff --git a/lib/page_demo_package/readme.md b/lib/page_demo_package/readme.md new file mode 100644 index 00000000..be5a40f1 --- /dev/null +++ b/lib/page_demo_package/readme.md @@ -0,0 +1,20 @@ +# 目录说明 + +本目录文件结构与文件命名, 使用cli进行更新 + + +# demos 目录文件结构 + +``` +demos +├── ${demoName}-${author}-${32位demoID} +│   ├── index.dart +│   └── src +│   ├── .demo.json +│   └── ${demoName}.dart +├── ...${demoName}-${author}-${32位demoID} +├── index.dart +├── info.json +└── readme.md +``` + \ No newline at end of file diff --git a/lib/resources/widget_name_to_icon.dart b/lib/resources/widget_name_to_icon.dart index 644d145d..82f2593f 100644 --- a/lib/resources/widget_name_to_icon.dart +++ b/lib/resources/widget_name_to_icon.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; class WidgetName2Icon { static Map icons = { + "Developer": Icons.developer_mode, + "Standard": Icons.pages , "Element":Icons.explicit, "Components":Icons.extension, - "Themes":Icons.filter_b_and_w, + "Theme":Icons.filter_b_and_w, "Form":Icons.table_chart, "Frame":Icons.aspect_ratio, "Media":Icons.subscriptions, diff --git a/lib/routers/application.dart b/lib/routers/application.dart index 8a03d9c7..0724ad89 100644 --- a/lib/routers/application.dart +++ b/lib/routers/application.dart @@ -1,12 +1,21 @@ import 'package:flutter/material.dart'; import 'package:fluro/fluro.dart'; - import 'package:flutter_go/utils/shared_preferences.dart'; +import '../model/widget.dart'; +enum ENV { + PRODUCTION, + DEV, +} class Application { + /// 通过Application设计环境变量 + static ENV env = ENV.DEV; + static Router router; static TabController controller; - static SpUtil sharePeferences; + static SpUtil sharePeference; + static CategoryComponent widgetTree; + static bool pageIsOpen = false; static Map github = { 'widgetsURL':'https://github.com/alibaba/flutter-go/blob/develop/lib/widgets/', @@ -14,4 +23,15 @@ class Application { //'master':'https://github.com/alibaba-paimai-frontend/flutter-common-widgets-app/tree/master/lib/widgets/' }; + /// 所有获取配置的唯一入口 + Map get config { + if (Application.env == ENV.PRODUCTION) { + return {}; + } + if (Application.env == ENV.DEV) { + return {}; + } + return {}; + } + } diff --git a/lib/routers/router_handler.dart b/lib/routers/router_handler.dart index e9f7698e..cd655b92 100644 --- a/lib/routers/router_handler.dart +++ b/lib/routers/router_handler.dart @@ -8,38 +8,66 @@ import 'package:flutter_go/views/web_page/web_view_page.dart'; import 'package:flutter_go/views/home.dart'; import 'package:flutter_go/views/login_page/login_page.dart'; +import 'package:flutter_go/model/user_info.dart'; +import 'package:flutter_go/views/collection_page/collection_page.dart'; +import 'package:flutter_go/views/collection_page/collection_full_page.dart'; +import 'package:flutter_go/views/standard_demo_page/index.dart'; +import 'package:flutter_go/views/issuse_message_page/issuse_message_page.dart'; // app的首页 var homeHandler = new Handler( handlerFunc: (BuildContext context, Map> params) { - return new AppPage(); + return new AppPage(UserInformation(id: 0)); }, ); +var collectionFullHandler = new Handler( + handlerFunc: (BuildContext context,Map> params){ + bool hasLogined = params['hasLogin']?.first == 'true'; + return CollectionFullPage(hasLogined: hasLogined); + } +); + +var collectionHandler = new Handler( + handlerFunc: (BuildContext context,Map> params){ + bool hasLogined = params['hasLogin']?.first == 'true'; + return CollectionPage(hasLogined: hasLogined); + } +); + var categoryHandler = new Handler( handlerFunc: (BuildContext context, Map> params) { - String name = params["type"]?.first; + String ids = params["ids"]?.first; - return new CategoryHome(name); + return new CategoryHome(ids); }, ); var widgetNotFoundHandler = new Handler( handlerFunc: (BuildContext context, Map> params) { - return new WidgetNotFound(); -}); + return new WidgetNotFound(); + }); var loginPageHandler = new Handler( handlerFunc: (BuildContext context, Map> params) { - return LoginPage(); -}); + return LoginPage(); + }); var fullScreenCodeDialog = new Handler( handlerFunc: (BuildContext context, Map> params) { - String path = params['filePath']?.first; - return new FullScreenCodeDialog( - filePath: path, - ); -}); + String path = params['filePath']?.first; + return new FullScreenCodeDialog( + filePath: path, + ); + }); + + +var githubCodeDialog = new Handler( + handlerFunc: (BuildContext context, Map> params) { + String path = params['remotePath']?.first; + return new FullScreenCodeDialog( + remoteFilePath: path, + ); + }); var webViewPageHand = new Handler( handlerFunc: (BuildContext context, Map> params) { @@ -47,3 +75,17 @@ var webViewPageHand = new Handler( String url = params['url']?.first; return new WebViewPage(url, title); }); + + +var standardPageHandler = new Handler( + handlerFunc: (BuildContext context, Map> params) { + String id = params['id']?.first; + return StandardView(id: id); + } +); + + +var issuesMessageHandler = new Handler( + handlerFunc: (BuildContext context, Map> params) { + return IssuesMessagePage(); + }); diff --git a/lib/routers/routers.dart b/lib/routers/routers.dart index 116fc33d..e21ddbae 100644 --- a/lib/routers/routers.dart +++ b/lib/routers/routers.dart @@ -3,39 +3,53 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; import 'package:flutter_go/utils/analytics.dart' show analytics; - import '../widgets/index.dart'; import './router_handler.dart'; - +/// import '../standard_pages/index.dart'; class Routes { static String root = "/"; static String home = "/home"; static String widgetDemo = '/widget-demo'; static String codeView = '/code-view'; + static String githubCodeView = '/github-code-view'; static String webViewPage = '/web-view-page'; static String loginPage = '/loginpage'; + static String issuesMessage='/issuesMessage'; + static String collectionPage = '/collection-page'; + static String collectionFullPage = '/collection-full-page'; + static String standardPage = '/standard-page/:id'; static void configureRoutes(Router router) { List widgetDemosList = new WidgetDemoList().getDemos(); - router.notFoundHandler = new Handler( - handlerFunc: (BuildContext context, Map> params) { - }); +// router.notFoundHandler = new Handler( +// handlerFunc: (BuildContext context, Map> params) { +// } +// ); router.define(home, handler: homeHandler); - - router.define('/category/:type', handler: categoryHandler); + router.define(collectionPage,handler:collectionHandler); + router.define(collectionFullPage,handler:collectionFullHandler); + router.define('/category/:ids', handler: categoryHandler); router.define('/category/error/404', handler: widgetNotFoundHandler); router.define(loginPage, handler: loginPageHandler); router.define(codeView,handler:fullScreenCodeDialog); + router.define(githubCodeView,handler:githubCodeDialog); router.define(webViewPage,handler:webViewPageHand); - widgetDemosList.forEach((demo) { - Handler handler = new Handler( - handlerFunc: (BuildContext context, Map> params) { - print('组件路由params=$params widgetsItem=${demo.routerName}'); - analytics.logEvent( - name: 'component', parameters: {'name': demo.routerName }); - return demo.buildRouter(context); - }); - router.define('${demo.routerName}', handler: handler); + router.define(issuesMessage, handler: issuesMessageHandler); + router.define(standardPage,handler:standardPageHandler); + widgetDemosList.forEach((demo) { + Handler handler = new Handler( + handlerFunc: (BuildContext context, Map> params) { + print('组件路由params=$params widgetsItem=${demo.routerName}'); + analytics.logEvent( + name: 'component', parameters: {'name': demo.routerName }); + return demo.buildRouter(context); + }); + String path = demo.routerName; + router.define('${path.toLowerCase()}', handler: handler); }); +// router.define(webViewPage,handler:webViewPageHand); +// standardPages.forEach((String id, String md) => { +// +// }); } } diff --git a/lib/standard_pages/.pages.json b/lib/standard_pages/.pages.json new file mode 100644 index 00000000..fa8e84db --- /dev/null +++ b/lib/standard_pages/.pages.json @@ -0,0 +1 @@ +[{"name":"PullToRefresh","screenShot":"","author":"chenfeihu","title":"PullToRefresh","email":"763551832@qq.com","desc":"Refresh conponent","id":"cd9b8b80_52ae_4241_9c8a_5c9e1f92b096"},{"name":"local","screenShot":"","author":"hnaxu","title":"本地","email":"hanxu@qq.com","desc":"desc","id":"5d7178d0_42ae_4241_9c8a_5c9e1f92b096"},{"name":"test","screenShot":"","author":"abc","title":"ya","email":"adsf.com","desc":"desc","id":"84f38e00_42ae_4241_9c8a_5c9e1f92b096"},{"name":"standard","screenShot":"","author":"sanfan","title":"介绍页","email":"hanxu317@qq.com","desc":"desc","id":"ee4feb8e_32ae_4241_9c8a_5c9e1f92b096"},{"name":"standard_for_slider","screenShot":"","author":"sanfan","title":"slider组件","email":"hanxu@qq.com","desc":"slider, new Slider","id":"8ab2b5c2_42ae_4241_9c8a_5c9e1f92b096"},{"name":"RangeSlider","screenShot":"","author":"hanxu","title":"RangeSlider","email":"hanxu317@qq.com","desc":"RangeSlider widget","id":"cbffbf7c_52ae_4241_9c8a_5c9e1f92b096"}] \ No newline at end of file diff --git a/lib/standard_pages/PullToRefresh_chenfeihu_cd9b8b80_52ae_4241_9c8a_5c9e1f92b096/.page.json b/lib/standard_pages/PullToRefresh_chenfeihu_cd9b8b80_52ae_4241_9c8a_5c9e1f92b096/.page.json new file mode 100644 index 00000000..b007469b --- /dev/null +++ b/lib/standard_pages/PullToRefresh_chenfeihu_cd9b8b80_52ae_4241_9c8a_5c9e1f92b096/.page.json @@ -0,0 +1,10 @@ +{ + "name": "PullToRefresh", + "screenShot": "", + "author":"chenfeihu", + "title":"PullToRefresh", + "email": "763551832@qq.com", + "desc": "Refresh conponent", + "id": "cd9b8b80_52ae_4241_9c8a_5c9e1f92b096" +} + \ No newline at end of file diff --git a/lib/standard_pages/PullToRefresh_chenfeihu_cd9b8b80_52ae_4241_9c8a_5c9e1f92b096/index.dart b/lib/standard_pages/PullToRefresh_chenfeihu_cd9b8b80_52ae_4241_9c8a_5c9e1f92b096/index.dart new file mode 100644 index 00000000..f675dbc6 --- /dev/null +++ b/lib/standard_pages/PullToRefresh_chenfeihu_cd9b8b80_52ae_4241_9c8a_5c9e1f92b096/index.dart @@ -0,0 +1,87 @@ +String getMd() { + return """ +# PullToRefresh + +> 下拉刷新 上拉加载 + +PullToRefresh 是一个刷新列表组件,借助于pull_to_refresh库实现,感觉这是目前最好的一款三方刷新库,它的可定制性比较好,刷新样式多样化,已经满足大部分的开发需求。 + +### **基本用法** + +* 添加依赖 pull_to_refresh: ^1.5.6 +* ListView包裹一层SmartRefresher + + + +### **SmartRefresher常用属性说明** + +* **enablePullDown** 允许下拉刷新 +* **enablePullUp** 允许上拉加载 +* **header** 下拉刷新头部样式 +* **footer** 上拉加载底部样式 +* **onRefresh** 下拉刷新的回调 +* **onLoading** 上拉加载的回调 +* **controller** 刷新控件的控制器,用来处理回调状态等 + + +### **国际化显示** + + ``` + 需要添加语言本地化SDK,不然刷新库头部与底部显示加载提示内容为英文 + + 添加依赖 flutter_localizations: + sdk: flutter + + main.dart中MateriaApp里面添加以下内容 + + localizationsDelegates: [ + // 这行是关键 + RefreshLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalMaterialLocalizations.delegate + ], + supportedLocales: [ + const Locale('en'), + const Locale('zh'), + ], + localeResolutionCallback: + (Locale locale, Iterable supportedLocales) { + //print("change language"); + return locale; + }, + + ``` + + +### **全局配置** + 全局配置RefreshConfiguration,配置子树下的所有SmartRefresher表现,一般存放于MaterialApp的根部 + + ``` + RefreshConfiguration( + headerBuilder: () => WaterDropHeader(), // 配置默认头部指示器,假如你每个页面的头部指示器都一样的话,你需要设置这个 + footerBuilder: () => ClassicFooter(), // 配置默认底部指示器 + headerTriggerDistance: 80.0, // 头部触发刷新的越界距离 + springDescription:SpringDescription(stiffness: 170, damping: 16, mass: 1.9), // 自定义回弹动画,三个属性值意义请查询flutter api + maxOverScrollExtent :100, //头部最大可以拖动的范围,如果发生冲出视图范围区域,请设置这个属性 + maxUnderScrollExtent:0, // 底部最大可以拖动的范围 + enableScrollWhenRefreshCompleted: true, //这个属性不兼容PageView和TabBarView,如果你特别需要TabBarView左右滑动,你需要把它设置为true + enableLoadingWhenFailed : true, //在加载失败的状态下,用户仍然可以通过手势上拉来触发加载更多 + hideFooterWhenNotFull: false, // Viewport不满一屏时,禁用上拉加载更多功能 + enableBallisticLoad: true, // 可以通过惯性滑动触发加载更多 + child: MaterialApp( + ........ + ) + ); + + ``` + + +### 实例展示 + +[demo:5553db80_52ae_4241_9c8a_5c9e1f92b096] + + + +"""; + +} diff --git a/lib/standard_pages/PullToRefresh_chenfeihu_cd9b8b80_52ae_4241_9c8a_5c9e1f92b096/index.md b/lib/standard_pages/PullToRefresh_chenfeihu_cd9b8b80_52ae_4241_9c8a_5c9e1f92b096/index.md new file mode 100644 index 00000000..1dca085b --- /dev/null +++ b/lib/standard_pages/PullToRefresh_chenfeihu_cd9b8b80_52ae_4241_9c8a_5c9e1f92b096/index.md @@ -0,0 +1,82 @@ +# PullToRefresh + +> 下拉刷新 上拉加载 + +PullToRefresh 是一个刷新列表组件,借助于pull_to_refresh库实现,感觉这是目前最好的一款三方刷新库,它的可定制性比较好,刷新样式多样化,已经满足大部分的开发需求。 + +### **基本用法** + +* 添加依赖 pull_to_refresh: ^1.5.6 +* ListView包裹一层SmartRefresher + + + +### **SmartRefresher常用属性说明** + +* **enablePullDown** 允许下拉刷新 +* **enablePullUp** 允许上拉加载 +* **header** 下拉刷新头部样式 +* **footer** 上拉加载底部样式 +* **onRefresh** 下拉刷新的回调 +* **onLoading** 上拉加载的回调 +* **controller** 刷新控件的控制器,用来处理回调状态等 + + +### **国际化显示** + + ``` + 需要添加语言本地化SDK,不然刷新库头部与底部显示加载提示内容为英文 + + 添加依赖 flutter_localizations: + sdk: flutter + + main.dart中MateriaApp里面添加以下内容 + + localizationsDelegates: [ + // 这行是关键 + RefreshLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalMaterialLocalizations.delegate + ], + supportedLocales: [ + const Locale('en'), + const Locale('zh'), + ], + localeResolutionCallback: + (Locale locale, Iterable supportedLocales) { + //print("change language"); + return locale; + }, + + ``` + + +### **全局配置** + 全局配置RefreshConfiguration,配置子树下的所有SmartRefresher表现,一般存放于MaterialApp的根部 + + ``` + RefreshConfiguration( + headerBuilder: () => WaterDropHeader(), // 配置默认头部指示器,假如你每个页面的头部指示器都一样的话,你需要设置这个 + footerBuilder: () => ClassicFooter(), // 配置默认底部指示器 + headerTriggerDistance: 80.0, // 头部触发刷新的越界距离 + springDescription:SpringDescription(stiffness: 170, damping: 16, mass: 1.9), // 自定义回弹动画,三个属性值意义请查询flutter api + maxOverScrollExtent :100, //头部最大可以拖动的范围,如果发生冲出视图范围区域,请设置这个属性 + maxUnderScrollExtent:0, // 底部最大可以拖动的范围 + enableScrollWhenRefreshCompleted: true, //这个属性不兼容PageView和TabBarView,如果你特别需要TabBarView左右滑动,你需要把它设置为true + enableLoadingWhenFailed : true, //在加载失败的状态下,用户仍然可以通过手势上拉来触发加载更多 + hideFooterWhenNotFull: false, // Viewport不满一屏时,禁用上拉加载更多功能 + enableBallisticLoad: true, // 可以通过惯性滑动触发加载更多 + child: MaterialApp( + ........ + ) + ); + + ``` + + +### 实例展示 + +[demo:5553db80_52ae_4241_9c8a_5c9e1f92b096] + + + \ No newline at end of file diff --git a/lib/standard_pages/RangeSlider_hanxu_cbffbf7c_52ae_4241_9c8a_5c9e1f92b096/.page.json b/lib/standard_pages/RangeSlider_hanxu_cbffbf7c_52ae_4241_9c8a_5c9e1f92b096/.page.json new file mode 100644 index 00000000..553fa67b --- /dev/null +++ b/lib/standard_pages/RangeSlider_hanxu_cbffbf7c_52ae_4241_9c8a_5c9e1f92b096/.page.json @@ -0,0 +1,10 @@ +{ + "name": "RangeSlider", + "screenShot": "", + "author":"hanxu", + "title":"RangeSlider", + "email": "hanxu317@qq.com", + "desc": "RangeSlider widget", + "id": "cbffbf7c_52ae_4241_9c8a_5c9e1f92b096" +} + \ No newline at end of file diff --git a/lib/standard_pages/RangeSlider_hanxu_cbffbf7c_52ae_4241_9c8a_5c9e1f92b096/index.dart b/lib/standard_pages/RangeSlider_hanxu_cbffbf7c_52ae_4241_9c8a_5c9e1f92b096/index.dart new file mode 100644 index 00000000..63850c68 --- /dev/null +++ b/lib/standard_pages/RangeSlider_hanxu_cbffbf7c_52ae_4241_9c8a_5c9e1f92b096/index.dart @@ -0,0 +1,36 @@ +String getMd() { + return """ + # RangeSlider + +> 用来选择范围性的数据 + +slider 用来选择连续性的或者非连续性的数据. 默认是在一段最大值最小值间做任意值的选择. 如果你想选择间隔性的值, 例如0.0到50.0间,选择10, 15,...50.0这样的值, 给divisions设定一个非空的整数5,, 去分割区间范围. + + +### **基本用法** + +关于slider有以下的术语: + +* **thumb** 滑块 用户可以水平拖拽移动的区域 + +* **track** 滑轨 thumb 可以滑动的线条区域 + +* **value indicator** 值指示器 当用户拖拽thumb的时候. 显示用户当前所选的属性值 + +* **active** 选中区 + +* **inactive** 非选中区 + +如果**onChanged**属性为空或者**min** .. **max**给出的范围 为空(例如如果min等于max),则将禁用滑块。 + +滑块小部件本身不保持任何状态State。相反,当滑块状态发生变化时,窗口小部件会调用 **onChanged** 回调。大多数使用滑块的小部件将侦听 **onChanged** 回调并使用新值重建滑块以更新滑块的视觉外观。要知道值何时开始更改,或何时更改,请设置可选回调**onChangeStart**或**onChangeEnd**。 + +默认情况下,滑块将尽可能宽,垂直居中。当给定无限制约束时,它将尝试使轨道宽144像素(每边有边距)并垂直收缩。 + +### 实例展示 + +[demo:e5f958bc_52ae_4241_9c8a_5c9e1f92b096]"""; + + +} + \ No newline at end of file diff --git a/lib/standard_pages/RangeSlider_hanxu_cbffbf7c_52ae_4241_9c8a_5c9e1f92b096/index.md b/lib/standard_pages/RangeSlider_hanxu_cbffbf7c_52ae_4241_9c8a_5c9e1f92b096/index.md new file mode 100644 index 00000000..d6049358 --- /dev/null +++ b/lib/standard_pages/RangeSlider_hanxu_cbffbf7c_52ae_4241_9c8a_5c9e1f92b096/index.md @@ -0,0 +1,30 @@ +# RangeSlider + +> 用来选择范围性的数据 + +slider 用来选择连续性的或者非连续性的数据. 默认是在一段最大值最小值间做任意值的选择. 如果你想选择间隔性的值, 例如0.0到50.0间,选择10, 15,...50.0这样的值, 给divisions设定一个非空的整数5,, 去分割区间范围. + + +### **基本用法** + +关于slider有以下的术语: + +* **thumb** 滑块 用户可以水平拖拽移动的区域 + +* **track** 滑轨 thumb 可以滑动的线条区域 + +* **value indicator** 值指示器 当用户拖拽thumb的时候. 显示用户当前所选的属性值 + +* **active** 选中区 + +* **inactive** 非选中区 + +如果**onChanged**属性为空或者**min** .. **max**给出的范围 为空(例如如果min等于max),则将禁用滑块。 + +滑块小部件本身不保持任何状态State。相反,当滑块状态发生变化时,窗口小部件会调用 **onChanged** 回调。大多数使用滑块的小部件将侦听 **onChanged** 回调并使用新值重建滑块以更新滑块的视觉外观。要知道值何时开始更改,或何时更改,请设置可选回调**onChangeStart**或**onChangeEnd**。 + +默认情况下,滑块将尽可能宽,垂直居中。当给定无限制约束时,它将尝试使轨道宽144像素(每边有边距)并垂直收缩。 + +### 实例展示 + +[demo:e5f958bc_52ae_4241_9c8a_5c9e1f92b096] \ No newline at end of file diff --git a/lib/standard_pages/index.dart b/lib/standard_pages/index.dart new file mode 100644 index 00000000..199d81f0 --- /dev/null +++ b/lib/standard_pages/index.dart @@ -0,0 +1,45 @@ + +import 'PullToRefresh_chenfeihu_cd9b8b80_52ae_4241_9c8a_5c9e1f92b096/index.dart' as StandardPage_PullToRefresh_cd9b8b80_52ae_4241_9c8a_5c9e1f92b096; +import 'local_hnaxu_5d7178d0_42ae_4241_9c8a_5c9e1f92b096/index.dart' as StandardPage_local_5d7178d0_42ae_4241_9c8a_5c9e1f92b096; +import 'test_abc_84f38e00_42ae_4241_9c8a_5c9e1f92b096/index.dart' as StandardPage_test_84f38e00_42ae_4241_9c8a_5c9e1f92b096; +import 'standard_sanfan_ee4feb8e_32ae_4241_9c8a_5c9e1f92b096/index.dart' as StandardPage_standard_ee4feb8e_32ae_4241_9c8a_5c9e1f92b096; +import 'standard_for_slider_sanfan_8ab2b5c2_42ae_4241_9c8a_5c9e1f92b096/index.dart' as StandardPage_standard_for_slider_8ab2b5c2_42ae_4241_9c8a_5c9e1f92b096; +import 'RangeSlider_hanxu_cbffbf7c_52ae_4241_9c8a_5c9e1f92b096/index.dart' as StandardPage_RangeSlider_cbffbf7c_52ae_4241_9c8a_5c9e1f92b096; +class StandardPages { + Map standardPages; + Map getPages() { + return { + "0": "0" , + "cd9b8b80_52ae_4241_9c8a_5c9e1f92b096" : StandardPage_PullToRefresh_cd9b8b80_52ae_4241_9c8a_5c9e1f92b096.getMd() +, + "5d7178d0_42ae_4241_9c8a_5c9e1f92b096" : StandardPage_local_5d7178d0_42ae_4241_9c8a_5c9e1f92b096.getMd() +, + "84f38e00_42ae_4241_9c8a_5c9e1f92b096" : StandardPage_test_84f38e00_42ae_4241_9c8a_5c9e1f92b096.getMd() +, + "ee4feb8e_32ae_4241_9c8a_5c9e1f92b096" : StandardPage_standard_ee4feb8e_32ae_4241_9c8a_5c9e1f92b096.getMd() +, + "8ab2b5c2_42ae_4241_9c8a_5c9e1f92b096" : StandardPage_standard_for_slider_8ab2b5c2_42ae_4241_9c8a_5c9e1f92b096.getMd() +, + "cbffbf7c_52ae_4241_9c8a_5c9e1f92b096" : StandardPage_RangeSlider_cbffbf7c_52ae_4241_9c8a_5c9e1f92b096.getMd() + }; + } + List> getLocalList() { + return [ + {}, + { "id": "cd9b8b80_52ae_4241_9c8a_5c9e1f92b096", "name": "PullToRefresh", "email": "763551832@qq.com", "author": "chenfeihu"} +, + { "id": "5d7178d0_42ae_4241_9c8a_5c9e1f92b096", "name": "local", "email": "hanxu@qq.com", "author": "hnaxu"} +, + { "id": "84f38e00_42ae_4241_9c8a_5c9e1f92b096", "name": "test", "email": "adsf.com", "author": "abc"} +, + { "id": "ee4feb8e_32ae_4241_9c8a_5c9e1f92b096", "name": "standard", "email": "hanxu317@qq.com", "author": "sanfan"} +, + { "id": "8ab2b5c2_42ae_4241_9c8a_5c9e1f92b096", "name": "standard_for_slider", "email": "hanxu@qq.com", "author": "sanfan"} +, + { "id": "cbffbf7c_52ae_4241_9c8a_5c9e1f92b096", "name": "RangeSlider", "email": "hanxu317@qq.com", "author": "hanxu"} + ]; + } + +} + + \ No newline at end of file diff --git a/lib/standard_pages/local_hnaxu_5d7178d0_42ae_4241_9c8a_5c9e1f92b096/.page.json b/lib/standard_pages/local_hnaxu_5d7178d0_42ae_4241_9c8a_5c9e1f92b096/.page.json new file mode 100644 index 00000000..d6a9e3d0 --- /dev/null +++ b/lib/standard_pages/local_hnaxu_5d7178d0_42ae_4241_9c8a_5c9e1f92b096/.page.json @@ -0,0 +1,10 @@ +{ + "name": "local", + "screenShot": "", + "author":"hnaxu", + "title":"本地", + "email": "hanxu@qq.com", + "desc": "desc", + "id": "5d7178d0_42ae_4241_9c8a_5c9e1f92b096" +} + \ No newline at end of file diff --git a/lib/standard_pages/local_hnaxu_5d7178d0_42ae_4241_9c8a_5c9e1f92b096/index.dart b/lib/standard_pages/local_hnaxu_5d7178d0_42ae_4241_9c8a_5c9e1f92b096/index.dart new file mode 100644 index 00000000..6e506d10 --- /dev/null +++ b/lib/standard_pages/local_hnaxu_5d7178d0_42ae_4241_9c8a_5c9e1f92b096/index.dart @@ -0,0 +1,52 @@ +String getMd() { + return """ + # 标准的详情页 + +您可以在这个界面中, 编写大多数的markdown文案, 他会在 **goCli watch** 下同步被编译成 **dart** 文件 + +您可以通过goCli创建详情页所需要的demo + +``` +goCLi createDemo +``` + +在flutter go 根文件下通过命令行输入以上命令可以进行以下操作 + +[✓] 请输入新增加的demo名称? demoName + +[✓] 请输入您的姓名(使用英文) yourName + +[✓] 请输入您的github的email地址 yourEmail + +[✓] 请输入您demo的描述 这是一个测试的标准demo + + +在完成以上操作后, 可以得到这样的输出: + + +``` +------------------ +您新增的组件信息如下 +================== +{ + name : demoName + author : yourName + email : yourEmail + desc : 这是一个测试的标准demo +} +================== +[✓] Is this the config you want ? (Y/n) y +{ + 新建的demo文件位于 : /flutter go/lib/page_demo_package/demoName_yourName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096 + demoId为 : 1a29aa8e_32ae_4241_9c8a_5c9e1f92b096 + markdown中调用方式 : [demo:1a29aa8e_32ae_4241_9c8a_5c9e1f92b096] +} + +``` +您可以在任意详情页中, 通过以下方式调用 + +``` +[demo: 1a29aa8e_32ae_4241_9c8a_5c9e1f92b096] +```"""; + +} diff --git a/lib/standard_pages/local_hnaxu_5d7178d0_42ae_4241_9c8a_5c9e1f92b096/index.md b/lib/standard_pages/local_hnaxu_5d7178d0_42ae_4241_9c8a_5c9e1f92b096/index.md new file mode 100644 index 00000000..28d32949 --- /dev/null +++ b/lib/standard_pages/local_hnaxu_5d7178d0_42ae_4241_9c8a_5c9e1f92b096/index.md @@ -0,0 +1,47 @@ +# 标准的详情页 this is test +您可以在这个界面中, 编写大多数的markdown文案, 他会在 **goCli watch** 下同步被编译成 **dart** 文件 + +您可以通过goCli创建详情页所需要的demo + +``` +goCLi createDemo +``` + +在flutter go 根文件下通过命令行输入以上命令可以进行以下操作 + +[✓] 请输入新增加的demo名称? demoName + +[✓] 请输入您的姓名(使用英文) yourName + +[✓] 请输入您的github的email地址 yourEmail + +[✓] 请输入您demo的描述 这是一个测试的标准demo + + +在完成以上操作后, 可以得到这样的输出: + + +``` +------------------ +您新增的组件信息如下 +================== +{ + name : demoName + author : yourName + email : yourEmail + desc : 这是一个测试的标准demo +} +================== +[✓] Is this the config you want ? (Y/n) y +{ + 新建的demo文件位于 : /flutter go/lib/page_demo_package/demoName_yourName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096 + demoId为 : 1a29aa8e_32ae_4241_9c8a_5c9e1f92b096 + markdown中调用方式 : [demo:1a29aa8e_32ae_4241_9c8a_5c9e1f92b096] +} + +``` +您可以在任意详情页中, 通过以下方式调用 + +``` +[demo: 1a29aa8e_32ae_4241_9c8a_5c9e1f92b096] +``` diff --git a/lib/standard_pages/standard_for_slider_sanfan_8ab2b5c2_42ae_4241_9c8a_5c9e1f92b096/.page.json b/lib/standard_pages/standard_for_slider_sanfan_8ab2b5c2_42ae_4241_9c8a_5c9e1f92b096/.page.json new file mode 100644 index 00000000..fa8a3a6a --- /dev/null +++ b/lib/standard_pages/standard_for_slider_sanfan_8ab2b5c2_42ae_4241_9c8a_5c9e1f92b096/.page.json @@ -0,0 +1,10 @@ +{ + "name": "standard_for_slider", + "screenShot": "", + "author":"sanfan", + "title":"slider组件", + "email": "hanxu@qq.com", + "desc": "slider, new Slider", + "id": "8ab2b5c2_42ae_4241_9c8a_5c9e1f92b096" +} + \ No newline at end of file diff --git a/lib/standard_pages/standard_for_slider_sanfan_8ab2b5c2_42ae_4241_9c8a_5c9e1f92b096/index.dart b/lib/standard_pages/standard_for_slider_sanfan_8ab2b5c2_42ae_4241_9c8a_5c9e1f92b096/index.dart new file mode 100644 index 00000000..bd222ee8 --- /dev/null +++ b/lib/standard_pages/standard_for_slider_sanfan_8ab2b5c2_42ae_4241_9c8a_5c9e1f92b096/index.dart @@ -0,0 +1,61 @@ +String getMd() { + return """ + # Slider Page + +您可以在这个界面中, 编写大多数的markdown文案, 他会在 **goCli watch** 下同步被编译成 **dart** 文件 + +您可以通过goCli创建详情页所需要的demo + + + + +``` +goCLi createDemo +``` + +在flutter go 根文件下通过命令行输入以上命令可以进行以下操作 + +[✓] 请输入新增加的demo名称? demoName + +[✓] 请输入您的姓名(使用英文) yourName + +[✓] 请输入您的github的email地址 yourEmail + +[✓] 请输入您demo的描述 这是一个测试的标准demo + + +在完成以上操作后, 可以得到这样的输出: + + +``` +------------------ +您新增的组件信息如下 +================== +{ + name : demoName + author : yourName + email : yourEmail + desc : 这是一个测试的标准demo +} +================== +[✓] Is this the config you want ? (Y/n) y +{ + 新建的demo文件位于 : /flutter go/lib/page_demo_package/demoName_yourName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096 + demoId为 : 1a29aa8e_32ae_4241_9c8a_5c9e1f92b096 + markdown中调用方式 : [demo:1a29aa8e_32ae_4241_9c8a_5c9e1f92b096] +} + +``` +您可以在任意详情页中, 通过以下方式调用 + +``` +[demo:1a29aa8e_32ae_4241_9c8a_5c9e1f92b096] +``` + +调用效果: + +[demo:1a29aa8e_32ae_4241_9c8a_5c9e1f92b096]"""; + + +} + \ No newline at end of file diff --git a/lib/standard_pages/standard_for_slider_sanfan_8ab2b5c2_42ae_4241_9c8a_5c9e1f92b096/index.md b/lib/standard_pages/standard_for_slider_sanfan_8ab2b5c2_42ae_4241_9c8a_5c9e1f92b096/index.md new file mode 100644 index 00000000..0cf3597e --- /dev/null +++ b/lib/standard_pages/standard_for_slider_sanfan_8ab2b5c2_42ae_4241_9c8a_5c9e1f92b096/index.md @@ -0,0 +1,55 @@ +# Slider Page + +您可以在这个界面中, 编写大多数的markdown文案, 他会在 **goCli watch** 下同步被编译成 **dart** 文件 + +您可以通过goCli创建详情页所需要的demo + + + + +``` +goCLi createDemo +``` + +在flutter go 根文件下通过命令行输入以上命令可以进行以下操作 + +[✓] 请输入新增加的demo名称? demoName + +[✓] 请输入您的姓名(使用英文) yourName + +[✓] 请输入您的github的email地址 yourEmail + +[✓] 请输入您demo的描述 这是一个测试的标准demo + + +在完成以上操作后, 可以得到这样的输出: + + +``` +------------------ +您新增的组件信息如下 +================== +{ + name : demoName + author : yourName + email : yourEmail + desc : 这是一个测试的标准demo +} +================== +[✓] Is this the config you want ? (Y/n) y +{ + 新建的demo文件位于 : /flutter go/lib/page_demo_package/demoName_yourName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096 + demoId为 : 1a29aa8e_32ae_4241_9c8a_5c9e1f92b096 + markdown中调用方式 : [demo:1a29aa8e_32ae_4241_9c8a_5c9e1f92b096] +} + +``` +您可以在任意详情页中, 通过以下方式调用 + +``` +[demo:1a29aa8e_32ae_4241_9c8a_5c9e1f92b096] +``` + +调用效果: + +[demo:1a29aa8e_32ae_4241_9c8a_5c9e1f92b096] \ No newline at end of file diff --git a/lib/standard_pages/standard_sanfan_ee4feb8e_32ae_4241_9c8a_5c9e1f92b096/.page.json b/lib/standard_pages/standard_sanfan_ee4feb8e_32ae_4241_9c8a_5c9e1f92b096/.page.json new file mode 100644 index 00000000..1e6d54b7 --- /dev/null +++ b/lib/standard_pages/standard_sanfan_ee4feb8e_32ae_4241_9c8a_5c9e1f92b096/.page.json @@ -0,0 +1,10 @@ +{ + "name": "standard", + "screenShot": "", + "author":"sanfan", + "title":"介绍页", + "email": "hanxu317@qq.com", + "desc": "desc", + "id": "ee4feb8e_32ae_4241_9c8a_5c9e1f92b096" +} + \ No newline at end of file diff --git a/lib/standard_pages/standard_sanfan_ee4feb8e_32ae_4241_9c8a_5c9e1f92b096/index.dart b/lib/standard_pages/standard_sanfan_ee4feb8e_32ae_4241_9c8a_5c9e1f92b096/index.dart new file mode 100644 index 00000000..be0e2607 --- /dev/null +++ b/lib/standard_pages/standard_sanfan_ee4feb8e_32ae_4241_9c8a_5c9e1f92b096/index.dart @@ -0,0 +1,56 @@ +String getMd() { + return """ + # 标准的详情页 + +您可以在这个界面中, 编写大多数的markdown文案, 他会在 **goCli watch** 下同步被编译成 **dart** 文件 + +您可以通过goCli创建详情页所需要的demo +[demo:1a29aa8e_32ae_4241_9c8a_5c9e1f92b096] + + +``` +goCLi createDemo +``` + +在flutter go 根文件下通过命令行输入以上命令可以进行以下操作 + +[✓] 请输入新增加的demo名称? demoName + +[✓] 请输入您的姓名(使用英文) yourName + +[✓] 请输入您的github的email地址 yourEmail + +[✓] 请输入您demo的描述 这是一个测试的标准demo + + +在完成以上操作后, 可以得到这样的输出: + + +``` +------------------ +您新增的组件信息如下 +================== +{ + name : demoName + author : yourName + email : yourEmail + desc : 这是一个测试的标准demo +} +================== +[✓] Is this the config you want ? (Y/n) y +{ + 新建的demo文件位于 : /flutter go/lib/page_demo_package/demoName_yourName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096 + demoId为 : 1a29aa8e_32ae_4241_9c8a_5c9e1f92b096 + markdown中调用方式 : [demo:1a29aa8e_32ae_4241_9c8a_5c9e1f92b096] +} + +``` +您可以在任意详情页中, 通过以下方式调用 + +``` +[demo: 1a29aa8e_32ae_4241_9c8a_5c9e1f92b096] +```"""; + + +} + \ No newline at end of file diff --git a/lib/standard_pages/standard_sanfan_ee4feb8e_32ae_4241_9c8a_5c9e1f92b096/index.md b/lib/standard_pages/standard_sanfan_ee4feb8e_32ae_4241_9c8a_5c9e1f92b096/index.md new file mode 100644 index 00000000..bf94471b --- /dev/null +++ b/lib/standard_pages/standard_sanfan_ee4feb8e_32ae_4241_9c8a_5c9e1f92b096/index.md @@ -0,0 +1,50 @@ +# 标准的详情页 this is title + +您可以在这个界面中, 编写大多数的markdown文案, 他会在 **goCli watch** 下同步被编译成 **dart** 文件 + +您可以通过goCli创建详情页所需要的demo +[demo:1a29aa8e_32ae_4241_9c8a_5c9e1f92b096] + + +``` +goCLi createDemo +``` + +在flutter go 根文件下通过命令行输入以上命令可以进行以下操作 + +[✓] 请输入新增加的demo名称? demoName + +[✓] 请输入您的姓名(使用英文) yourName + +[✓] 请输入您的github的email地址 yourEmail + +[✓] 请输入您demo的描述 这是一个测试的标准demo + + +在完成以上操作后, 可以得到这样的输出: + + +``` +------------------ +您新增的组件信息如下 +================== +{ + name : demoName + author : yourName + email : yourEmail + desc : 这是一个测试的标准demo +} +================== +[✓] Is this the config you want ? (Y/n) y +{ + 新建的demo文件位于 : /flutter go/lib/page_demo_package/demoName_yourName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096 + demoId为 : 1a29aa8e_32ae_4241_9c8a_5c9e1f92b096 + markdown中调用方式 : [demo:1a29aa8e_32ae_4241_9c8a_5c9e1f92b096] +} + +``` +您可以在任意详情页中, 通过以下方式调用 + +``` +[demo: 1a29aa8e_32ae_4241_9c8a_5c9e1f92b096] +``` diff --git a/lib/standard_pages/test_abc_84f38e00_42ae_4241_9c8a_5c9e1f92b096/.page.json b/lib/standard_pages/test_abc_84f38e00_42ae_4241_9c8a_5c9e1f92b096/.page.json new file mode 100644 index 00000000..297b026e --- /dev/null +++ b/lib/standard_pages/test_abc_84f38e00_42ae_4241_9c8a_5c9e1f92b096/.page.json @@ -0,0 +1,10 @@ +{ + "name": "test", + "screenShot": "", + "author":"abc", + "title":"ya", + "email": "adsf.com", + "desc": "desc", + "id": "84f38e00_42ae_4241_9c8a_5c9e1f92b096" +} + \ No newline at end of file diff --git a/lib/standard_pages/test_abc_84f38e00_42ae_4241_9c8a_5c9e1f92b096/index.dart b/lib/standard_pages/test_abc_84f38e00_42ae_4241_9c8a_5c9e1f92b096/index.dart new file mode 100644 index 00000000..6e506d10 --- /dev/null +++ b/lib/standard_pages/test_abc_84f38e00_42ae_4241_9c8a_5c9e1f92b096/index.dart @@ -0,0 +1,52 @@ +String getMd() { + return """ + # 标准的详情页 + +您可以在这个界面中, 编写大多数的markdown文案, 他会在 **goCli watch** 下同步被编译成 **dart** 文件 + +您可以通过goCli创建详情页所需要的demo + +``` +goCLi createDemo +``` + +在flutter go 根文件下通过命令行输入以上命令可以进行以下操作 + +[✓] 请输入新增加的demo名称? demoName + +[✓] 请输入您的姓名(使用英文) yourName + +[✓] 请输入您的github的email地址 yourEmail + +[✓] 请输入您demo的描述 这是一个测试的标准demo + + +在完成以上操作后, 可以得到这样的输出: + + +``` +------------------ +您新增的组件信息如下 +================== +{ + name : demoName + author : yourName + email : yourEmail + desc : 这是一个测试的标准demo +} +================== +[✓] Is this the config you want ? (Y/n) y +{ + 新建的demo文件位于 : /flutter go/lib/page_demo_package/demoName_yourName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096 + demoId为 : 1a29aa8e_32ae_4241_9c8a_5c9e1f92b096 + markdown中调用方式 : [demo:1a29aa8e_32ae_4241_9c8a_5c9e1f92b096] +} + +``` +您可以在任意详情页中, 通过以下方式调用 + +``` +[demo: 1a29aa8e_32ae_4241_9c8a_5c9e1f92b096] +```"""; + +} diff --git a/lib/standard_pages/test_abc_84f38e00_42ae_4241_9c8a_5c9e1f92b096/index.md b/lib/standard_pages/test_abc_84f38e00_42ae_4241_9c8a_5c9e1f92b096/index.md new file mode 100644 index 00000000..61a5f516 --- /dev/null +++ b/lib/standard_pages/test_abc_84f38e00_42ae_4241_9c8a_5c9e1f92b096/index.md @@ -0,0 +1,48 @@ +# 标准的详情页 + +您可以在这个界面中, 编写大多数的markdown文案, 他会在 **goCli watch** 下同步被编译成 **dart** 文件 + +您可以通过goCli创建详情页所需要的demo + +``` +goCLi createDemo +``` + +在flutter go 根文件下通过命令行输入以上命令可以进行以下操作 + +[✓] 请输入新增加的demo名称? demoName + +[✓] 请输入您的姓名(使用英文) yourName + +[✓] 请输入您的github的email地址 yourEmail + +[✓] 请输入您demo的描述 这是一个测试的标准demo + + +在完成以上操作后, 可以得到这样的输出: + + +``` +------------------ +您新增的组件信息如下 +================== +{ + name : demoName + author : yourName + email : yourEmail + desc : 这是一个测试的标准demo +} +================== +[✓] Is this the config you want ? (Y/n) y +{ + 新建的demo文件位于 : /flutter go/lib/page_demo_package/demoName_yourName_1a29aa8e_32ae_4241_9c8a_5c9e1f92b096 + demoId为 : 1a29aa8e_32ae_4241_9c8a_5c9e1f92b096 + markdown中调用方式 : [demo:1a29aa8e_32ae_4241_9c8a_5c9e1f92b096] +} + +``` +您可以在任意详情页中, 通过以下方式调用 + +``` +[demo: 1a29aa8e_32ae_4241_9c8a_5c9e1f92b096] +``` \ No newline at end of file diff --git a/lib/utils/data_utils.dart b/lib/utils/data_utils.dart index 149c6e22..921f95ff 100644 --- a/lib/utils/data_utils.dart +++ b/lib/utils/data_utils.dart @@ -1,26 +1,83 @@ import 'dart:async' show Future; +import 'package:fluro/fluro.dart'; +import 'package:flutter_go/model/collection.dart'; import 'package:flutter_go/model/version.dart'; +import 'package:flutter_go/model/widget.dart'; import 'package:package_info/package_info.dart'; -import 'package:url_launcher/url_launcher.dart'; +/// import 'package:flutter_go/model/responseData.dart'; import './net_utils.dart'; import '../model/user_info.dart'; import 'package:flutter_go/api/api.dart'; +import 'package:flutter_go/routers/application.dart'; +import 'package:flutter_go/routers/routers.dart'; class DataUtils { // 登陆获取用户信息 - static Future doLogin(Map params) async { + static Future doLogin(Map params) async { var response = await NetUtils.post(Api.DO_LOGIN, params); - UserInfo userInfo = UserInfo.fromJson(response['data']); - return userInfo; + try { + UserInformation userInfo = UserInformation.fromJson(response['data']); + return userInfo; + } catch (err) { + return response['message']; + } + } + + // 获取用户信息 + static Future getUserInfo(Map params) async { + var response = await NetUtils.get(Api.GET_USER_INFO, params); + try { + UserInformation userInfo = UserInformation.fromJson(response['data']); + return userInfo; + } catch (err) { + return response['message']; + } } // 验证登陆 - - static Future checkLogin() async { + static Future checkLogin() async { var response = await NetUtils.get(Api.CHECK_LOGIN); - print('验证登陆:$response'); + print('response: $response'); + try { + if (response['success']) { + print('${response['success']} ${response['data']} response[succes]'); + UserInformation userInfo = UserInformation.fromJson(response['data']); + print('${response['data']} $userInfo'); + return userInfo; + } else { + return response['success']; + } + } catch (err) { + return response['message']; + } + } + + // 一键反馈 + static Future feedback(Map params, context) async { + var response = await NetUtils.post(Api.FEEDBACK, params); + if (response['status'] == 401 && response['message'] == '请先登录') { + Application.router.navigateTo(context, '${Routes.loginPage}', + transition: TransitionType.nativeModal); + } + return response['success']; + } + + //设置主题颜色 + static Future setThemeColor(int color, context) async { + var response = + await NetUtils.post(Api.SET_THEMECOLOR, {'color': color.toString()}); + if (response['status'] == 401 && response['message'] == '请先登录') { + Application.router.navigateTo(context, '${Routes.loginPage}', + transition: TransitionType.nativeModal); + } + return response['success']; + } + + //获取主题颜色 + static Future getThemeColor() async { + var response = await NetUtils.get(Api.GET_THEMECOLOR); return response['success']; } @@ -31,9 +88,6 @@ class DataUtils { return response['success']; } - /** - * {"status":200,"data":{"version":"0.0.2","name":"FlutterGo"},"success":true} - */ // 检查版本 static Future checkVersion(Map params) async { var response = await NetUtils.get(Api.VERSION, params); @@ -42,12 +96,107 @@ class DataUtils { PackageInfo packageInfo = await PackageInfo.fromPlatform(); var localVersion = packageInfo.version; //相同=0、大于=1、小于=-1 -// localVersion = '0.0.2'; -// currVersion = '1.0.6'; + // localVersion = '0.0.2'; + // currVersion = '1.0.6'; if (currVersion.compareTo(localVersion) == 1) { return true; } else { return false; } } + + /// 获取widget列表处的树型数据 + static Future getWidgetTreeList() async { + try { + var response = await NetUtils.get(Api.GET_WIDGET_TREE); + print('组件树dddd:$response'); + if (response != null && response['success']) { + return response['data']; + } else { + return []; + } + } catch (error) { + print('获取组件树 error $error'); + return []; + } + } + + // 校验是否收藏 + static Future checkCollected(Map params) async { + print('url 地址:${Api.CHECK_COLLECTED} $params'); + try { + var response = await NetUtils.post(Api.CHECK_COLLECTED, params); + return response != null && response['hasCollected']; + } catch (error) { + print('校验收藏 error $error'); + return false; + } + } + + // 添加收藏 + static Future addCollected(Map params, context) async { + var response = await NetUtils.post(Api.ADD_COLLECTION, params); + if (response['status'] == 401 && response['message'] == '请先登录') { + Application.router.navigateTo(context, '${Routes.loginPage}', + transition: TransitionType.nativeModal); + } + return response != null && response['success']; + } + + // 移出收藏 + static Future removeCollected(Map params, context) async { + var response = await NetUtils.post(Api.REMOVE_COLLECTION, params); + if (response['status'] == 401 && response['message'] == '请先登录') { + Application.router.navigateTo(context, '${Routes.loginPage}', + transition: TransitionType.nativeModal); + } + return response != null && response['success']; + } + + // 获取全部收藏 + static Future getAllCollections(context) async { + var response = await NetUtils.get(Api.GET_ALL_COLLECTION); + List responseList = []; + if (response['status'] == 401 && response['message'] == '请先登录') { + Application.router.navigateTo(context, '${Routes.loginPage}', + transition: TransitionType.nativeModal); + } + if (response != null && response['success'] == true) { + for (int i = 0; i < response['data'].length; i++) { + Map tempCo = response['data'][i]; + responseList.add(Collection.fromJSON( + {"name": tempCo['name'], "router": tempCo['url']})); + } + return responseList; + } else { + return []; + } + } + + // 搜索组件 + static Future searchWidget(String name) async { + var response = await NetUtils.get(Api.SEARCH_WIDGET, {"name": name}); + List list = []; + if (response != null && response['success'] == true) { + for (int i = 0; i < response['data'].length; i++) { + var json = response['data'][i]; + String routerName; + if (json['display'] == 'old') { + routerName = json['path']; + } else { + routerName = json['pageId']; + } + Map tempMap = { + "name": json['name'], + "cnName": json['name'], + "routerName": routerName, + "catId": json['parentId'].runtimeType == String ? int.parse(json['parentId']) : json['parentId'] + }; + list.add(WidgetPoint.fromJSON(tempMap)); + } + return list; + } else { + return []; + } + } } diff --git a/lib/utils/net_utils.dart b/lib/utils/net_utils.dart index 2f39746c..b93c00d1 100644 --- a/lib/utils/net_utils.dart +++ b/lib/utils/net_utils.dart @@ -4,15 +4,14 @@ import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; import 'package:path_provider/path_provider.dart'; -Map optHeader = { - 'accept-language':'zh-cn', - 'content-type':'application/json' +Map optHeader = { + 'accept-language': 'zh-cn', + 'content-type': 'application/json' }; -var dio = new Dio(BaseOptions(connectTimeout: 30000,headers: optHeader)); +var dio = new Dio(BaseOptions(connectTimeout: 30000, headers: optHeader)); class NetUtils { - static Future get(String url, [Map params]) async { var response; @@ -20,7 +19,7 @@ class NetUtils { // (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = // (HttpClient client) { // client.findProxy = (uri) { - // return "PROXY 30.10.26.193:8888"; + // return "PROXY 30.10.24.79:8889"; // }; // }; @@ -28,7 +27,6 @@ class NetUtils { String documentsPath = documentsDir.path; var dir = new Directory("$documentsPath/cookies"); await dir.create(); - // print('documentPath:${dir.path}'); dio.interceptors.add(CookieManager(PersistCookieJar(dir: dir.path))); if (params != null) { response = await dio.get(url, queryParameters: params); @@ -39,6 +37,18 @@ class NetUtils { } static Future post(String url, Map params) async { + // // 设置代理 便于本地 charles 抓包 + // (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = + // (HttpClient client) { + // client.findProxy = (uri) { + // return "PROXY 30.10.24.79:8889"; + // }; + // }; + Directory documentsDir = await getApplicationDocumentsDirectory(); + String documentsPath = documentsDir.path; + var dir = new Directory("$documentsPath/cookies"); + await dir.create(); + dio.interceptors.add(CookieManager(PersistCookieJar(dir: dir.path))); var response = await dio.post(url, data: params); return response.data; } diff --git a/lib/utils/shared_preferences.dart b/lib/utils/shared_preferences.dart index 81f7dc70..c7351a66 100644 --- a/lib/utils/shared_preferences.dart +++ b/lib/utils/shared_preferences.dart @@ -24,8 +24,9 @@ class SpUtil { static Future getInstance() async { if (_instance == null) { _instance = new SpUtil._(); + } + if (_spf == null) { await _instance._init(); - } return _instance; } @@ -117,4 +118,4 @@ class SpUtil { if (_beforeCheck()) return null; return _spf.clear(); } -} \ No newline at end of file +} diff --git a/lib/views/collection_page/collection_full_page.dart b/lib/views/collection_page/collection_full_page.dart new file mode 100644 index 00000000..ada616eb --- /dev/null +++ b/lib/views/collection_page/collection_full_page.dart @@ -0,0 +1,161 @@ +/// @Author: 一凨 +/// @Date: 2019-06-05 14:01:03 +/// @Last Modified by: 一凨 +/// @Last Modified time: 2019-06-05 14:01:03 +import 'package:flutter/material.dart'; +import 'package:event_bus/event_bus.dart'; +import 'package:fluro/fluro.dart'; + +import 'package:flutter_go/model/collection.dart'; +import 'package:flutter_go/routers/application.dart'; +import 'package:flutter_go/event/event_bus.dart'; +import 'package:flutter_go/event/event_model.dart'; +import 'package:flutter_go/utils/data_utils.dart'; + +class CollectionFullPage extends StatefulWidget { + final bool hasLogined; + CollectionFullPage({Key key, this.hasLogined}) : super(key: key); + + @override + _CollectionFullPageState createState() => _CollectionFullPageState(); +} + +class _CollectionFullPageState extends State { + _CollectionFullPageState() { + final eventBus = new EventBus(); + ApplicationEvent.event = eventBus; + } + + /// CollectionControlModel _collectionControl = new CollectionControlModel(); + List _collectionList = []; + ScrollController _scrollController = new ScrollController(); + var _icons; + + @override + void initState() { + super.initState(); + _getList(); + ApplicationEvent.event.on().listen((event) { + _getList(); + }); + } + + void _getList() { + _collectionList.clear(); + DataUtils.getAllCollections(context).then((collectionList) { + if (this.mounted) { + setState(() { + _collectionList = collectionList; + }); + } + }); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + Widget _renderList(context, index) { + + if (index == 0) { + return Container( + height: 40.0, + padding: const EdgeInsets.only(left: 10.0), + child: Row( + children: [ + Icon( + Icons.warning, + size: 22.0, + ), + SizedBox( + width: 5.0, + ), + Text('常用的组件都可以收藏在这里哦'), + ], + ), + ); + } + if (_collectionList[index - 1].router.contains('http')) { + if (_collectionList[index - 1].name.endsWith('Doc')) { + _icons = Icons.library_books; + } else { + _icons = Icons.language; + } + } else { + _icons = Icons.extension; + } + String targetRouter = _collectionList[index - 1].router; + return Container( + padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 5.0), + margin: const EdgeInsets.only(bottom: 7.0), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + new BoxShadow( + color: const Color(0xFFd0d0d0), + blurRadius: 1.0, + spreadRadius: 2.0, + offset: Offset(3.0, 2.0), + ), + ], + ), + child: ListTile( + leading: Icon( + _icons, + size: 30.0, + color: Theme.of(context).primaryColor, + ), + title: Text( + _collectionList[index - 1].name, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 17.0), + ), + trailing: + Icon(Icons.keyboard_arrow_right, color: Colors.grey, size: 30.0), + onTap: () { + Application.router.navigateTo( + context, targetRouter.toLowerCase(), + transition: TransitionType.inFromRight); + }, + ), + ); + } + + ListView buildContent() { + if (_collectionList.length == 0) { + return ListView( + children: [ + Column( + children: [ + Image.asset( + 'assets/images/nothing.png', + fit: BoxFit.contain, + width: MediaQuery.of(context).size.width / 2, + ), + Text('暂无收藏,赶紧去收藏一个吧!'), + ], + ), + ], + ); + } + return ListView.builder( + itemBuilder: _renderList, + itemCount: _collectionList.length + 1, + controller: _scrollController, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('我的收藏'), + ), + body: Container( + child: buildContent(), + ), + ); + } +} diff --git a/lib/views/collection_page/collection_page.dart b/lib/views/collection_page/collection_page.dart index 6b4c609b..75d6e309 100644 --- a/lib/views/collection_page/collection_page.dart +++ b/lib/views/collection_page/collection_page.dart @@ -1,5 +1,5 @@ -/// @Author: 一凨 -/// @Date: 2019-01-08 17:12:58 +/// @Author: 一凨 +/// @Date: 2019-01-08 17:12:58 /// @Last Modified by: 一凨 /// @Last Modified time: 2019-01-14 20:13:28 @@ -11,9 +11,13 @@ import 'package:flutter_go/routers/application.dart'; import 'package:flutter_go/routers/routers.dart'; import 'package:flutter_go/event/event_bus.dart'; import 'package:flutter_go/event/event_model.dart'; - +/// import 'package:flutter_go/utils/data_utils.dart'; class CollectionPage extends StatefulWidget { + final bool hasLogined; + + CollectionPage({Key key, this.hasLogined}) : super(key: key); + _CollectionPageState createState() => _CollectionPageState(); } @@ -22,7 +26,7 @@ class _CollectionPageState extends State { final eventBus = new EventBus(); ApplicationEvent.event = eventBus; } - CollectionControlModel _collectionControl = new CollectionControlModel(); + /// CollectionControlModel _collectionControl = new CollectionControlModel(); List _collectionList = []; ScrollController _scrollController = new ScrollController(); var _icons; @@ -44,16 +48,13 @@ class _CollectionPageState extends State { void _getList() { _collectionList.clear(); - _collectionControl.getAllCollection().then((resultList) { - resultList.forEach((item) { - _collectionList.add(item); - }); - if (this.mounted) { - setState(() { - _collectionList = _collectionList; - }); - } - }); + // DataUtils.getAllCollections(context).then((collectionList) { + // if (this.mounted) { + // setState(() { + // _collectionList = collectionList; + // }); + // } + // }); } Widget _renderList(context, index) { @@ -105,7 +106,8 @@ class _CollectionPageState extends State { color: Theme.of(context).primaryColor, ), title: Text( - Uri.decodeComponent(_collectionList[index - 1].name), + _collectionList[index - 1].name, +// Uri.decodeComponent(_collectionList[index - 1].name), overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 17.0), ), diff --git a/lib/views/first_page/drawer_page.dart b/lib/views/first_page/drawer_page.dart new file mode 100644 index 00000000..f0fc1be0 --- /dev/null +++ b/lib/views/first_page/drawer_page.dart @@ -0,0 +1,247 @@ +import 'dart:async'; + +import 'package:fluro/fluro.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_go/components/single_theme_color.dart'; +import 'package:flutter_go/model/user_info.dart'; +import 'package:share/share.dart'; +import 'package:flutter_go/utils/data_utils.dart'; +import 'package:flutter_go/routers/application.dart'; +import 'package:flutter_go/routers/routers.dart'; +import './search_page.dart'; +import 'package:flutter_go/event/event_bus.dart'; +import 'package:flutter_go/event/event_model.dart'; +import 'package:event_bus/event_bus.dart'; + +const List> defalutThemeColor = [ + {'cnName': 'Flutter篮', 'value': 0xFF3391EA}, + {'cnName': '拍卖红', 'value': 0xFFC91B3A}, + {'cnName': '阿里橙', 'value': 0xFFF7852A}, +]; + +class DrawerPage extends StatefulWidget { + final UserInformation userInfo; + DrawerPage({Key key, this.userInfo}) : super(key: key); + + @override + _DrawerPageState createState() => _DrawerPageState(); +} + +class _DrawerPageState extends State { + final TextStyle textStyle = + TextStyle(fontSize: 16, fontWeight: FontWeight.w300); + bool hasLogin; + + _DrawerPageState() { + final eventBus = new EventBus(); + ApplicationEvent.event = eventBus; + } + + @override + void initState() { + super.initState(); + ApplicationEvent.event.on().listen((event) { + print('接收到的 event ${event.settingThemeColor}'); + }); + hasLogin = this.widget.userInfo.id != 0; + } + + Future logoutDialog(BuildContext context) { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('确认退出登陆?'), + // content: Text('退出登陆后将没法进行'), + actions: [ + FlatButton( + onPressed: () { + // 退出登陆 + DataUtils.logout().then((result) { + if (result) { + Application.router.navigateTo( + context, '${Routes.loginPage}', + transition: TransitionType.native, clearStack: true); + } + }); + }, + child: Text( + '确认', + style: TextStyle(color: Colors.red), + ), + ), + FlatButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text('取消'), + ) + ], + ); + }); + } + + void showLogoutDialog(BuildContext context) { + if (hasLogin) { + logoutDialog(context); + } else { + Application.router.navigateTo(context, '${Routes.loginPage}', + transition: TransitionType.native, clearStack: true); + } + } + + void pushPage(BuildContext context, Widget page, {String pageName}) { + if (context == null || page == null) return; + Navigator.push(context, CupertinoPageRoute(builder: (ctx) => page)); + } + + Future buildSimpleDialog(BuildContext context) { + return showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 20.0), + height: 300.0, + color: Colors.white, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + mainAxisSize: MainAxisSize.max, + children: buildThemeColorChildren(), + )), + ); + }); + } + + List buildThemeColorChildren() { + List tempWidget = []; + for (var i = 0; i < defalutThemeColor.length; i++) { + tempWidget.add(SingleThemeColor( + themeColor: defalutThemeColor[i]['value'], + coloeName: defalutThemeColor[i]['cnName'], + )); + } + return tempWidget; + } + + @override + Widget build(BuildContext context) { + return ListView( + padding: EdgeInsets.zero, + children: [ + UserAccountsDrawerHeader( + accountName: Text(''), + accountEmail: Container( + padding: const EdgeInsets.only(bottom: 20.0), + child: Text( + hasLogin ? widget.userInfo.username : ' ', + style: TextStyle(fontSize: 28), + ), + ), + decoration: BoxDecoration( + image: new DecorationImage( + fit: BoxFit.cover, + image: new NetworkImage(hasLogin + ? widget.userInfo.avatarPic + : 'https://hbimg.huabanimg.com/9bfa0fad3b1284d652d370fa0a8155e1222c62c0bf9d-YjG0Vt_fw658'), + ), + ), + ), + // new Divider(), + ListTile( + leading: Icon( + Icons.search, + size: 27.0, + ), + title: Text( + '全网搜', + style: textStyle, + ), + onTap: () { + pushPage(context, SearchPage(), pageName: "SearchPage"); + }, + ), + ListTile( + leading: Icon( + Icons.favorite, + size: 27.0, + ), + title: Text( + '我的收藏', + style: textStyle, + ), + onTap: () { + Application.router.navigateTo(context, + '${Routes.collectionFullPage}?hasLogin=${hasLogin.toString()}', + transition: TransitionType.fadeIn); + }, + ), + // new Divider(), + // ListTile( + // leading: Icon( + // Icons.settings, + // size: 27.0, + // ), + // title: Text( + // '主题色', + // style: textStyle, + // ), + // onTap: () { + // buildSimpleDialog(context); + // }, + // ), + new Divider(), + + ListTile( + leading: Icon( + Icons.email, + size: 27.0, + ), + title: Text( + '反馈/建议', + style: textStyle, + ), + onTap: () { + if (hasLogin) { + //issue 未登陆状态 返回登陆页面 + Application.router.navigateTo(context, '${Routes.issuesMessage}'); + } else { + //No description provided. + Application.router.navigateTo(context, '${Routes.loginPage}'); + + } + }, + ), + ListTile( + leading: Icon( + Icons.share, + size: 27.0, + ), + title: Text( + '分享 App', + style: textStyle, + ), + onTap: () { + Share.share('https://flutter-go.pub/website/'); + }, + ), + new Divider(), + ListTile( + leading: Icon( + hasLogin ? Icons.exit_to_app : Icons.supervised_user_circle, + size: 27.0, + ), + title: Text( + hasLogin ? '退出登陆' : '点击登录', + style: textStyle, + ), + onTap: () { + showLogoutDialog(context); + // logoutDialog(context); + }, + ), + ], + ); + } +} diff --git a/lib/views/first_page/first_page.dart b/lib/views/first_page/first_page.dart index bc052da6..13e87884 100644 --- a/lib/views/first_page/first_page.dart +++ b/lib/views/first_page/first_page.dart @@ -51,7 +51,9 @@ class FirstPageState extends State } Future getIndexListData([Map params]) async { - const juejin_flutter = 'https://timeline-merger-ms.juejin.im/v1/get_tag_entry?src=web&tagId=5a96291f6fb9a0535b535438'; + /// const juejin_flutter = 'https://timeline-merger-ms.juejin.im/v1/get_tag_entry?src=web&tagId=5a96291f6fb9a0535b535438'; + const juejin_flutter = 'https://fluttergo.pub:9527/juejin.im/v1/get_tag_entry?src=web&tagId=5a96291f6fb9a0535b535438'; + var pageIndex = (params is Map) ? params['pageIndex'] : 0; final _param = {'page': pageIndex, 'pageSize': 20, 'sort': 'rankIndex'}; var responseList = []; diff --git a/lib/views/first_page/main_page.dart b/lib/views/first_page/main_page.dart index 1b4f492d..c48fbe0d 100644 --- a/lib/views/first_page/main_page.dart +++ b/lib/views/first_page/main_page.dart @@ -1,41 +1,51 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter_go/views/first_page/drawer_page.dart'; import './first_page.dart'; -import './sub_page.dart'; +/// import './sub_page.dart'; import './main_app_bar.dart'; import './search_page.dart'; +import 'package:flutter_go/model/user_info.dart'; +import 'package:flutter_go/routers/application.dart' show Application; +import 'package:flutter_go/routers/routers.dart' show Routes; + +DefaultTabController _tabController; +TabBar _tabBar; class _Page { final String labelId; + final int labelIndex; - _Page(this.labelId); + _Page(this.labelId,this.labelIndex); } final List<_Page> _allPages = <_Page>[ - _Page('项目1'), - _Page('项目2'), - _Page('项目3'), - _Page('项目4'), + _Page('热门资讯', 1), + _Page('FG-官网', 2), + _Page('FG-web版', 3), + ///_Page('项目4'), ]; class MainPage extends StatelessWidget { + final UserInformation userInfo; + + MainPage({Key key, this.userInfo}) : super(key: key); @override Widget build(BuildContext context) { print("MainPagess build......"); - return DefaultTabController( + _tabController = DefaultTabController( length: _allPages.length, - child: Scaffold( - appBar: new MyAppBar( + child: Scaffold( + appBar: MyAppBar( leading: Container( - child: new ClipOval( - child: Image.network( - 'https://hbimg.huabanimg.com/9bfa0fad3b1284d652d370fa0a8155e1222c62c0bf9d-YjG0Vt_fw658', - scale: 15.0, - ), - ) - ), + child: ClipOval( + child: Image.network( + userInfo.id == 0?'https://hbimg.huabanimg.com/9bfa0fad3b1284d652d370fa0a8155e1222c62c0bf9d-YjG0Vt_fw658':userInfo.avatarPic, + scale: 15.0, + ), + )), centerTitle: true, - title: TabLayout(), + title: TabLayout(), actions: [ IconButton( icon: Icon(Icons.search), @@ -44,11 +54,14 @@ class MainPage extends StatelessWidget { }) ], ), - body: TabBarViewLayout(), -// drawer: Drawer( -// child: MainLeftPage(), -// ), + drawer: Drawer( + child: DrawerPage( + userInfo: userInfo + ), + ), + body: TabBarViewLayout(), )); + return _tabController; } } @@ -60,35 +73,38 @@ void pushPage(BuildContext context, Widget page, {String pageName}) { class TabLayout extends StatelessWidget { @override Widget build(BuildContext context) { - return TabBar( + _tabBar = TabBar( isScrollable: true, //labelPadding: EdgeInsets.all(12.0), - labelPadding: EdgeInsets.only(top: 12.0,left: 12.0,right:12.0), + labelPadding: EdgeInsets.only(top: 12.0, left: 12.0, right: 12.0), indicatorSize: TabBarIndicatorSize.label, - tabs: _allPages - .map((_Page page) => - Tab(text: page.labelId)) - .toList(), + tabs: _allPages.map((_Page page) => Tab(text: page.labelId)).toList(), + onTap: (index) { + if (index == 1) { + DefaultTabController.of(context).animateTo(0); + Application.router.navigateTo(context, '${Routes.webViewPage}?title=${Uri.encodeComponent('Flutter Go 官方网站')}&url=${Uri.encodeComponent('https://flutter-go.pub')}'); + } else if (index == 2) { + +// new Future.delayed(const Duration(seconds: 1),(){ +// showAlertDialog(Application.globalContext); +// }); + + DefaultTabController.of(context).animateTo(0); + Application.router.navigateTo(context, '${Routes.webViewPage}?title=${Uri.encodeComponent('Flutter Go web版(H5)')}&url=${Uri.encodeComponent('https://flutter-go.pub/flutter_go_web')}'); + } + } ); + return _tabBar; } } class TabBarViewLayout extends StatelessWidget { Widget buildTabView(BuildContext context, _Page page) { - String labelId = page.labelId; - switch (labelId) { - case '项目1': + int labelIndex = page.labelIndex; + switch (labelIndex) { + case 1: return FirstPage(); break; - case '项目2': - return SubPage(); - break; - case '项目3': - return SubPage(); - break; - case '项目4': - return SubPage(); - break; default: return Container(); break; diff --git a/lib/views/first_page/search_page.dart b/lib/views/first_page/search_page.dart index 6d519738..042bae8a 100644 --- a/lib/views/first_page/search_page.dart +++ b/lib/views/first_page/search_page.dart @@ -23,7 +23,7 @@ final _industryPage = Industry.IndustryPage(itemTitle: (state){ ), subtitle: Text(state.res[index].source), onTap: () { - // 在这里对选中的结果进行解析,因为我目前是用golang实现的,所以就没贴代码了。 + // 在这里对选中的结果进行解析 print(state.res[index].source); final itemTitle = state.res[index].title; final itemUrl = state.res[index].source; @@ -44,7 +44,7 @@ class SearchPage extends StatelessWidget { /// print('suggestion::${Industry.suggestion}'); return Scaffold( appBar: PreferredSize( - preferredSize: Size(double.infinity, 52), // 44 is the height + preferredSize: Size(double.infinity, 52), // is the height child: AppBar(title: searchBarPage) ), //body: ProgressView(), @@ -87,15 +87,17 @@ class SearchBarPage extends StatefulWidget{ final TextEditingController controller = TextEditingController(); var that; +var loading; class _SearchBarPageState extends State { @override void initState() { super.initState(); that = this; + loading = false; } Timer _resultsTimer; - bool _loading = false; + String oldKey; /// 防抖函数 Future getResultsDebounced(String text) async { @@ -106,12 +108,12 @@ class _SearchBarPageState extends State { if (text == '' || !mounted) { return; } - _loading = true; + loading = true; if (_resultsTimer != null && _resultsTimer.isActive) { _resultsTimer.cancel(); } _resultsTimer = new Timer(new Duration(milliseconds: 400), () async { - _loading = true; + loading = true; if(mounted){ suggestion.dispatch(SuggestionFetch(query: text)); } @@ -121,7 +123,7 @@ class _SearchBarPageState extends State { void onSearchTextChanged(String text){ - print('onSearchTextChanged:${text}'); + print('onSearchTextChanged:$text'); //suggestion.dispatch(SuggestionFetch(query: text)); getResultsDebounced(text); } @@ -160,7 +162,7 @@ class _SearchBarPageState extends State { controller: controller, decoration: InputDecoration( contentPadding: EdgeInsets.only(top: 0.0), - hintText: '搜索全局flutter知识库', + hintText: '全网搜索 Flutter 相关知识库', hintStyle:TextStyle(fontSize: 12.0), border: InputBorder.none ), diff --git a/lib/views/home.dart b/lib/views/home.dart index 3dca3861..f8e6815f 100644 --- a/lib/views/home.dart +++ b/lib/views/home.dart @@ -6,24 +6,32 @@ /// target: app首页 import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; +/// import 'package:flutter/rendering.dart'; +import 'package:flutter_go/utils/data_utils.dart'; import 'package:flutter_go/utils/shared_preferences.dart'; -import 'package:flutter_go/views/first_page/first_page.dart'; +/// import 'package:flutter_go/views/first_page/first_page.dart'; import 'package:flutter_go/views/first_page/main_page.dart'; +import 'package:fluro/fluro.dart'; +import 'package:flutter_go/views/user_page/user_page.dart'; import 'package:flutter_go/views/widget_page/widget_page.dart'; import 'package:flutter_go/views/welcome_page/fourth_page.dart'; -import 'package:flutter_go/views/collection_page/collection_page.dart'; +/// import 'package:flutter_go/views/collection_page/collection_page.dart'; import 'package:flutter_go/routers/application.dart'; -import 'package:flutter_go/utils/provider.dart'; +/// import 'package:flutter_go/utils/provider.dart'; import 'package:flutter_go/model/widget.dart'; -import 'package:flutter_go/widgets/index.dart'; +/// import 'package:flutter_go/widgets/index.dart'; import 'package:flutter_go/components/search_input.dart'; import 'package:flutter_go/model/search_history.dart'; import 'package:flutter_go/resources/widget_name_to_icon.dart'; +import 'package:flutter_go/model/user_info.dart'; + -const int ThemeColor = 0xFFC91B3A; class AppPage extends StatefulWidget { + final UserInformation userInfo; + + AppPage(this.userInfo); + @override State createState() { return _MyHomePageState(); @@ -36,22 +44,32 @@ class _MyHomePageState extends State WidgetControlModel widgetControl = new WidgetControlModel(); SearchHistoryList searchHistoryList; bool isSearch = false; - String appBarTitle = tabData[0]['text']; + List _list = List(); int _currentIndex = 0; - static List tabData = [ -// {'text': '业界动态', 'icon': Icon(Icons.language)}, + List tabData = [ {'text': 'WIDGET', 'icon': Icon(Icons.extension)}, - {'text': '组件收藏', 'icon': Icon(Icons.favorite)}, {'text': '关于手册', 'icon': Icon(Icons.import_contacts)}, + {'text': '个人中心', 'icon': Icon(Icons.account_circle)}, + //https://flutter-go.pub/api/isInfoOpen ]; - List _myTabs = []; + String appBarTitle; @override void initState() { super.initState(); + print('widget.userInfo ${widget.userInfo}'); initSearchHistory(); + + if(Application.pageIsOpen == true){// 是否展开业界动态 + tabData.insert(0, {'text': '业界动态', 'icon': Icon(Icons.language)}); + _list + //..add(FirstPage()) + ..add(MainPage(userInfo: widget.userInfo)); + } + appBarTitle = tabData[0]['text']; + for (int i = 0; i < tabData.length; i++) { _myTabs.add(BottomNavigationBarItem( icon: tabData[i]['icon'], @@ -60,12 +78,10 @@ class _MyHomePageState extends State ), )); } - _list -// ..add(FirstPage()) -// ..add(MainPage()) - ..add(WidgetPage(Provider.db)) - ..add(CollectionPage()) - ..add(FourthPage()); + _list + ..add(WidgetPage()) + ..add(FourthPage()) + ..add(UserPage(userInfo: widget.userInfo)); } @override @@ -81,26 +97,21 @@ class _MyHomePageState extends State } void onWidgetTap(WidgetPoint widgetPoint, BuildContext context) { - List widgetDemosList = new WidgetDemoList().getDemos(); String targetName = widgetPoint.name; - String targetRouter = '/category/error/404'; - widgetDemosList.forEach((item) { - if (item.name == targetName) { - targetRouter = item.routerName; - } - }); - searchHistoryList - .add(SearchHistory(name: targetName, targetRouter: targetRouter)); + String targetRouter = widgetPoint.routerName; + searchHistoryList.add( + SearchHistory(name: targetName, targetRouter: targetRouter)); print("searchHistoryList1 ${searchHistoryList.toString()}"); - print("searchHistoryList2 ${targetRouter}"); - print("searchHistoryList3 ${widgetPoint.name}"); - Application.router.navigateTo(context, "$targetRouter"); + Application.router.navigateTo(context, targetRouter.toLowerCase(), + transition: TransitionType.inFromRight); } Widget buildSearchInput(BuildContext context) { return new SearchInput((value) async { if (value != '') { - List list = await widgetControl.search(value); + print('value ::: $value'); + // List list = await widgetControl.search(value); + List list = await DataUtils.searchWidget(value); return list .map((item) => new MaterialSearchResult( value: item.name, @@ -118,11 +129,11 @@ class _MyHomePageState extends State } renderAppBar(BuildContext context, Widget widget, int index) { -// print('renderAppBar=====>>>>>>${index}'); -// if (index == 0) { -// return null; -// } - return AppBar(title: buildSearchInput(context)); + if (index == 1 && Application.pageIsOpen == true) { + return AppBar(title: buildSearchInput(context)); + }else if(index == 0 && Application.pageIsOpen == false){ + return AppBar(title: buildSearchInput(context)); + } } @override @@ -143,7 +154,7 @@ class _MyHomePageState extends State //fixed:固定 type: BottomNavigationBarType.fixed, - fixedColor: Color(0xFFC91B3A), + fixedColor: Theme.of(context).primaryColor, ), ); } diff --git a/lib/views/issuse_message_page/issuse_message_page.dart b/lib/views/issuse_message_page/issuse_message_page.dart new file mode 100644 index 00000000..0bfef825 --- /dev/null +++ b/lib/views/issuse_message_page/issuse_message_page.dart @@ -0,0 +1,151 @@ +/// import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:zefyr/zefyr.dart'; +import 'package:flutter_go/utils/data_utils.dart'; +import 'package:notus/convert.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + + + +class IssuesMessagePage extends StatefulWidget { + @override + _IssuesMessagePageState createState() => _IssuesMessagePageState(); +} + +class _IssuesMessagePageState extends State { + final TextEditingController _controller = new TextEditingController(); + final ZefyrController _zefyrController = new ZefyrController(NotusDocument()); + final FocusNode _focusNode = new FocusNode(); + String _title = ""; + var _delta; + + @override + void initState() { + _controller.addListener(() { + print("_controller.text:${_controller.text}"); + setState(() { + _title = _controller.text; + }); + }); + + _zefyrController.document.changes.listen((change) { + setState(() { + _delta = _zefyrController.document.toDelta(); + }); + }); + + super.initState(); + } + + void dispose() { + _controller.dispose(); + _zefyrController.dispose(); + super.dispose(); + } + + _submit() { + if (_title.trim().isEmpty) { + _show('标题不能为空'); + } else { + String mk = (_delta==null)?'No description provided.':notusMarkdown.encode(_delta); + DataUtils.feedback({'title': _title, "body": mk},context).then((result) { + _show('提交成功'); + Navigator.maybePop(context); + }); + } + } + + _show(String msgs){ + Fluttertoast.showToast( + msg: msgs, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.CENTER, + timeInSecForIos: 1, + backgroundColor: Theme.of(context).primaryColor, + textColor: Colors.white, + fontSize: 16.0); + } + + Widget buildLoading() { + return Opacity( + opacity: .5, + child: Container( + width: MediaQuery.of(context).size.width * 0.85, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + color: Colors.black, + ), + child: SpinKitPouringHourglass(color: Colors.white), + ), + ); + } + + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('反馈/意见'), + actions: [ + FlatButton.icon( + onPressed: () { + _submit(); + }, + icon: Icon( + Icons.near_me, + color: Colors.white, + size: 12, + ), + label: Text( + '发送', + style: TextStyle(color: Colors.white), + ), + ) + ], + elevation: 1.0, + ), + body: ZefyrScaffold( + child: Padding( + padding: EdgeInsets.all(8), + child: ListView( + children: [ + Text('输入标题:'), + new TextFormField( + maxLength: 50, + controller: _controller, + decoration: new InputDecoration( + hintText: 'Title', + ), + ), + Text('内容:'), + _descriptionEditor(), + ], + ), + ), + )); + } + + Widget _descriptionEditor() { + final theme = new ZefyrThemeData( + toolbarTheme: ZefyrToolbarTheme.fallback(context).copyWith( + color: Colors.grey.shade800, + toggleColor: Colors.grey.shade900, + iconColor: Colors.white, + disabledIconColor: Colors.grey.shade500, + ), + ); + + return ZefyrTheme( + data: theme, + child: ZefyrField( + height: 400.0, + decoration: InputDecoration(labelText: 'Description'), + controller: _zefyrController, + focusNode: _focusNode, + autofocus: true, + physics: ClampingScrollPhysics(), + ), + ); + } +} diff --git a/lib/views/login_page/login_page.dart b/lib/views/login_page/login_page.dart index 0e658ec8..a24d6625 100644 --- a/lib/views/login_page/login_page.dart +++ b/lib/views/login_page/login_page.dart @@ -1,9 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:event_bus/event_bus.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + import 'package:flutter_go/utils/data_utils.dart'; import 'package:flutter_go/views/home.dart'; +import 'package:flutter_go/event/event_bus.dart'; +import 'package:flutter_go/event/event_model.dart'; import 'package:flutter_go/model/user_info_cache.dart'; +import 'package:flutter_go/routers/application.dart'; +import 'package:flutter_go/routers/routers.dart'; +import 'package:flutter_go/model/user_info.dart'; class LoginPage extends StatefulWidget { @override @@ -11,6 +19,11 @@ class LoginPage extends StatefulWidget { } class _LoginPageState extends State { + _LoginPageState() { + final eventBus = new EventBus(); + ApplicationEvent.event = eventBus; + } + // 利用FocusNode和_focusScopeNode来控制焦点 可以通过FocusNode.of(context)来获取widget树中默认的_focusScopeNode FocusNode _emailFocusNode = new FocusNode(); FocusNode _passwordFocusNode = new FocusNode(); @@ -35,7 +48,6 @@ class _LoginPageState extends State { _userInfoControlModel.getAllInfo().then((list) { if (list.length > 0) { UserInfo _userInfo = list[0]; - print('获取的数据:${_userInfo.username} ${_userInfo.password}'); setState(() { _userNameEditingController.text = _userInfo.username; _passwordEditingController.text = _userInfo.password; @@ -47,27 +59,61 @@ class _LoginPageState extends State { } catch (err) { print('读取用户本地存储的用户信息出错 $err'); } + + ApplicationEvent.event.on().listen((event) { + print('loginName:${event.loginName} token:${event.token} 1234567'); + if (event.isSuccess == true) { + // oAuth 认证成功 + setState(() { + isLoading = true; + }); + DataUtils.getUserInfo( + {'loginName': event.loginName, 'token': event.token}) + .then((result) { + setState(() { + isLoading = false; + }); + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (context) => AppPage(result)), + (route) => route == null); + }).catchError((onError) { + print('获取身份信息 error:::$onError'); + setState(() { + isLoading = false; + }); + }); + } else { + Fluttertoast.showToast( + msg: '验证失败', + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.CENTER, + timeInSecForIos: 1, + backgroundColor: Theme.of(context).primaryColor, + textColor: Colors.white, + fontSize: 16.0); + } + }); } // 创建登录界面的TextForm Widget buildSignInTextForm() { - return new Container( - decoration: new BoxDecoration( + return Container( + decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(8)), ), width: MediaQuery.of(context).size.width * 0.8, height: 190, // * Flutter提供了一个Form widget,它可以对输入框进行分组,然后进行一些统一操作,如输入内容校验、输入框重置以及输入内容保存。 - child: new Form( + child: Form( key: _signInFormKey, - child: new Column( + child: Column( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Padding( padding: const EdgeInsets.only( left: 25, right: 25, top: 20, bottom: 20), - child: new TextFormField( + child: TextFormField( controller: _userNameEditingController, //关联焦点 focusNode: _emailFocusNode, @@ -78,19 +124,20 @@ class _LoginPageState extends State { _focusScopeNode.requestFocus(_passwordFocusNode); }, - decoration: new InputDecoration( - icon: new Icon( + decoration: InputDecoration( + icon: Icon( Icons.email, color: Colors.black, ), hintText: "Github 登录名", border: InputBorder.none), - style: new TextStyle(fontSize: 16, color: Colors.black), + style: TextStyle(fontSize: 16, color: Colors.black), //验证 validator: (value) { if (value.isEmpty) { return "登录名不可为空!"; } + return null; }, onSaved: (value) { setState(() { @@ -100,7 +147,7 @@ class _LoginPageState extends State { ), ), ), - new Container( + Container( height: 1, width: MediaQuery.of(context).size.width * 0.75, color: Colors.grey[400], @@ -108,18 +155,18 @@ class _LoginPageState extends State { Flexible( child: Padding( padding: const EdgeInsets.only(left: 25, right: 25, top: 20), - child: new TextFormField( + child: TextFormField( controller: _passwordEditingController, focusNode: _passwordFocusNode, - decoration: new InputDecoration( - icon: new Icon( + decoration: InputDecoration( + icon: Icon( Icons.lock, color: Colors.black, ), hintText: "Github 登录密码", border: InputBorder.none, - suffixIcon: new IconButton( - icon: new Icon( + suffixIcon: IconButton( + icon: Icon( Icons.remove_red_eye, color: Colors.black, ), @@ -128,11 +175,12 @@ class _LoginPageState extends State { ), //输入密码,需要用*****显示 obscureText: !isShowPassWord, - style: new TextStyle(fontSize: 16, color: Colors.black), + style: TextStyle(fontSize: 16, color: Colors.black), validator: (value) { if (value == null || value.isEmpty) { return "密码不可为空!"; } + return null; }, onSaved: (value) { setState(() { @@ -142,7 +190,7 @@ class _LoginPageState extends State { ), ), ), - new Container( + Container( height: 1, width: MediaQuery.of(context).size.width * 0.75, color: Colors.grey[400], @@ -154,15 +202,15 @@ class _LoginPageState extends State { } Widget buildSignInButton() { - return new GestureDetector( - child: new Container( + return GestureDetector( + child: Container( padding: EdgeInsets.only(left: 42, right: 42, top: 10, bottom: 10), - decoration: new BoxDecoration( + decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(5)), color: Theme.of(context).primaryColor), - child: new Text( + child: Text( "LOGIN", - style: new TextStyle(fontSize: 25, color: Colors.white), + style: TextStyle(fontSize: 25, color: Colors.white), ), ), onTap: () { @@ -171,7 +219,7 @@ class _LoginPageState extends State { // 如果输入都检验通过,则进行登录操作 // Scaffold.of(context) // .showSnackBar(new SnackBar(content: new Text("执行登录操作"))); - //调用所有自孩子的save回调,保存表单内容 + //调用所有自孩子��save回调,保存表单内容 doLogin(); } }, @@ -180,38 +228,56 @@ class _LoginPageState extends State { // 登陆操作 doLogin() { + print("doLogin"); _signInFormKey.currentState.save(); setState(() { isLoading = true; }); DataUtils.doLogin({'username': username, 'password': password}) - .then((result) { - print(result); + .then((userResult) { setState(() { isLoading = false; }); - try { - _userInfoControlModel.deleteAll().then((result) { - // print('删除结果:$result'); - _userInfoControlModel - .insert(UserInfo(password: password, username: username)) - .then((value) { - // print('存储成功:$value'); - Navigator.of(context).pushAndRemoveUntil( - new MaterialPageRoute(builder: (context) => AppPage()), - (route) => route == null); + if (userResult.runtimeType == UserInformation) { + try { + _userInfoControlModel.deleteAll().then((result) { + // print('删除结果:$result'); + _userInfoControlModel + .insert(UserInfo(password: password, username: username)) + .then((value) { + print('存储成功:$value'); + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (context) => AppPage(userResult)), + (route) => route == null); + }); }); - }); - } catch (err) { - Navigator.of(context).pushAndRemoveUntil( - new MaterialPageRoute(builder: (context) => AppPage()), - (route) => route == null); + } catch (err) { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (context) => AppPage(userResult)), + (route) => route == null); + } + }else if(userResult.runtimeType == String){ + Fluttertoast.showToast( + msg: userResult, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.CENTER, + timeInSecForIos: 1, + backgroundColor: Theme.of(context).primaryColor, + textColor: Colors.white, + fontSize: 16.0); } - }).catchError((onError) { - print(onError); + }).catchError((errorMsg) { setState(() { isLoading = false; }); + Fluttertoast.showToast( + msg: errorMsg.toString(), + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.CENTER, + timeInSecForIos: 1, + backgroundColor: Theme.of(context).primaryColor, + textColor: Colors.white, + fontSize: 16.0); }); } @@ -267,15 +333,66 @@ class _LoginPageState extends State { mainAxisSize: MainAxisSize.min, children: [ SizedBox(height: 35.0), - Image.asset( - 'assets/images/FlutterGo.png', - fit: BoxFit.contain, - width: 100.0, - height: 100.0, + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/images/gitHub.png', + fit: BoxFit.contain, + width: 60.0, + height: 60.0, + ), + Image.asset( + 'assets/images/arrow.png', + fit: BoxFit.contain, + width: 40.0, + height: 30.0, + ), + Image.asset( + 'assets/images/FlutterGo.png', + fit: BoxFit.contain, + width: 60.0, + height: 60.0, + ), + ], ), buildSignInTextForm(), buildSignInButton(), - SizedBox(height: 35.0), + SizedBox(height: 15.0), + new Container( + height: 1, + width: MediaQuery.of(context).size.width * 0.75, + color: Colors.grey[400], + margin: const EdgeInsets.only(bottom: 10.0), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FlatButton( + child: Text( + 'Github OAuth 认证', + style: TextStyle( + color: Theme.of(context).primaryColor, + decoration: TextDecoration.underline), + ), + onPressed: () { + Application.router.navigateTo(context, + '${Routes.webViewPage}?title=Github&url=${Uri.encodeComponent("https://github.com/login/oauth/authorize?client_id=cfe4795e76382ae8a5bd&scope=user,public_repo")}'); + }, + ), + FlatButton( + child: Text( + '游客登录', + style: TextStyle( + color: Theme.of(context).primaryColor, + decoration: TextDecoration.underline), + ), + onPressed: () { + Application.router.navigateTo(context, Routes.home, clearStack: true); + }, + ) + ], + ) ], ), Positioned( diff --git a/lib/views/standard_demo_page/index.dart b/lib/views/standard_demo_page/index.dart new file mode 100644 index 00000000..20264f9d --- /dev/null +++ b/lib/views/standard_demo_page/index.dart @@ -0,0 +1,239 @@ +// +// Created with Android Studio. +// User: 三帆 +// Date: 25/05/2019 +// Time: 21:46 +// email: sanfan.hx@alibaba-inc.com +// tartget: xxx +// + +import 'package:flutter/material.dart'; +import '../../components/widget_demo.dart'; +import 'dart:convert'; +import '../../components/markdown.dart' as mdCopy; +import '../../components/flutter_markdown/lib/flutter_markdown.dart'; +import '../../standard_pages/index.dart'; +import '../../page_demo_package/index.dart'; +import 'package:flutter_go/routers/application.dart'; +import 'package:flutter_go/routers/routers.dart'; +import 'package:flutter_go/utils/net_utils.dart'; +import 'package:flutter_go/components/loading.dart'; + +const githubHost = 'https://raw.githubusercontent.com/alibaba/flutter-go/master'; +const githubUrl = '$githubHost/lib/standard_pages/'; +const PagesUrl = '$githubHost/lib/standard_pages/.pages.json'; +const DemosUrl = '$githubHost/lib/page_demo_package/.demo.json'; + + +// ONLINE || LOCAL +ENV env = Application.env; + +class StandardView extends StatefulWidget { + final String id; + final String detailMd; + StandardView({this.id, this.detailMd}); + @override + _StandardView createState() => _StandardView(); +} + +class _StandardView extends State { + String markdownDesc = ''; + String pageTitle = ''; + bool isLoading = false; + String author = ''; + String email = ''; + StandardPages standardPage = new StandardPages(); + @override + void initState() { + super.initState(); + this.getPageInfo(); + } + +// didChangeDependencies() { +// print("didChangeDependencies"); +// } + /// 本地调用的获取文章属性的基本信息 + Future localGetPagesAttrsInfo() async { + String jsonString = await DefaultAssetBundle.of(context).loadString('lib/standard_pages/.pages.json'); + List jsonList = json.decode(jsonString); + Map pageDetail = jsonList.firstWhere((item) => item['id'] == widget.id, orElse: null); + + if (pageDetail != null) { + setState(() { + pageTitle = pageDetail['title'] ?? '请加入title'; + author = pageDetail['author']; + email = pageDetail['email']; + }); + } + } + + /// 从本地获取基本文章信息 + String localGetPagesMarkdown() { + + String pageId = widget.id; + Map pagesList = standardPage.getPages(); +// print('pagesList[pageId]>>> ${pagesList[pageId]}'); + return pagesList[pageId]; + } + Future getContentOnline() async { + this.setState(() { + isLoading = true; + }); + + List response = jsonDecode(await NetUtils.get(PagesUrl)); + + + + Map targetPage = response.firstWhere((page) => page['id'] == widget.id); + if (targetPage == null) { + setState(() { + isLoading = false; + }); + return Future(() => '未获取界面相当信息'); + } + setState(() { + pageTitle = targetPage['title'] ?? 'xxx'; + author = targetPage['author']; + email = targetPage['email']; + }); + + String pageName = targetPage['name'] + "_" +targetPage['author']+ "_" +targetPage['id']; + String pageContent = await NetUtils.get(githubUrl + pageName + "/index.md"); + setState(() { + isLoading = false; + }); + return Future(() => pageContent); + } + /// 获取当面界面的相关信息. 需要区分环境 + /// 本地环境下, 从本地获取 standard_pages的目录中互殴 + /// 线上环境. 从github的api中获取 + Future getPageInfo() async { + String conent = ''; + print("env:::: $env"); + + if (env == ENV.PRODUCTION) { + conent = await getContentOnline(); + } else { + conent = localGetPagesMarkdown(); + localGetPagesAttrsInfo(); + } + if (this.mounted) { + setState(() { + markdownDesc = conent; + }); + } + return Future(() => conent); + } + void seeSourceCode(id) async { + List response; + try { + response = jsonDecode(await NetUtils.get(DemosUrl)); + } catch (e) { + return alertDialog(msg: '请检查网络链接', title: '提示'); + } + + Map demoDetail = response.firstWhere((item) => item['id'] == id, orElse: null); + if (demoDetail == null) { + return null; + } + + String remoteSouceCode = '$githubHost/lib/page_demo_package/${demoDetail['name']}_${demoDetail['author']}_${demoDetail['id']}/src/index.dart'; + Application.router.navigateTo(context, + '${Routes.githubCodeView}?remotePath=${Uri.encodeComponent(remoteSouceCode)}'); + + } + Widget buildFootInfo() { + if (!isLoading) { + return Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('创建者: $author'), + Text('邮箱: $email'), + Text('界面id: ${widget.id}') + ], + ), + ); + } + return Container(); + } + + Widget buildMarkdown() { + + + if (markdownDesc == null) { + return null; + } else { + if (Application.env == ENV.DEV) { + // 为了能在local改变的时候. 动态更新内容, getPageInfo只有初始化状态下会有效果 + markdownDesc = localGetPagesMarkdown(); + } + } + + return MarkdownBody( + data: markdownDesc, + syntaxHighlighter:new mdCopy.HighLight(), + demoBuilder: (Map attrs) { + List demo = demoObjects[attrs['id']]; + if (demo == null) { + String errString = "not found ${attrs['id']} in demo packages"; + debugPrint(errString); + demo = [Text(errString)]; + return Column(children: demo); + } else { + return Column( + children: [ + Column( + children: demo, + ), + Divider( + color: Theme.of(context).primaryColor, + ), + InkWell( + onTap: () { + seeSourceCode(attrs['id']); + }, + child: Text("查看源码", style: TextStyle(color: Theme.of(context).primaryColor)), + ) + ], + ) ; + } + }); + } + alertDialog({String msg, String title}) { + showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return new AlertDialog( + title: new Text(title), + content: new SingleChildScrollView( + child: new ListBody( + children: [ + new Text(msg), + ], + ), + ) + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return new WidgetDemo( + title: pageTitle, +// codeUrl: 'elements/Form/Button/DropdownButton/demo.dart', + contentList: [ + NetLoadingDialog( + loading: isLoading, + outsideDismiss: false, + ), + buildMarkdown(), + SizedBox(height: 30), + buildFootInfo(), + SizedBox(height: 30) + ], + ); + } +} diff --git a/lib/views/user_page/user_page.dart b/lib/views/user_page/user_page.dart new file mode 100644 index 00000000..74924c58 --- /dev/null +++ b/lib/views/user_page/user_page.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_go/model/user_info.dart'; +import 'package:flutter_go/views/first_page/drawer_page.dart'; + +class UserPage extends StatefulWidget { + final UserInformation userInfo; + + UserPage({Key key, this.userInfo}) : super(key: key); + + @override + _UserPageState createState() => _UserPageState(); +} + +class _UserPageState extends State { + @override + Widget build(BuildContext context) { + print(widget.userInfo); + return Scaffold( + body: Container( + child: DrawerPage( + userInfo: widget.userInfo, + ), + ), + ); + } +} diff --git a/lib/views/web_page/web_view_page.dart b/lib/views/web_page/web_view_page.dart index df3793a5..8f39c7f6 100644 --- a/lib/views/web_page/web_view_page.dart +++ b/lib/views/web_page/web_view_page.dart @@ -1,5 +1,5 @@ -/// @Author: 一凨 -/// @Date: 2019-01-14 17:44:47 +/// @Author: 一凨 +/// @Date: 2019-01-14 17:44:47 /// @Last Modified by: 一凨 /// @Last Modified time: 2019-01-14 19:47:14 @@ -8,9 +8,10 @@ import 'dart:core'; import 'package:flutter/material.dart'; import 'package:flutter_webview_plugin/flutter_webview_plugin.dart'; -import 'package:flutter_go/model/collection.dart'; +/// import 'package:flutter_go/model/collection.dart'; import 'package:flutter_go/event/event_bus.dart'; import 'package:flutter_go/event/event_model.dart'; +import 'package:flutter_go/api/api.dart'; class WebViewPage extends StatefulWidget { final String url; @@ -21,95 +22,54 @@ class WebViewPage extends StatefulWidget { } class _WebViewPageState extends State { - bool _hasCollected = false; - String _router = ''; - var _collectionIcons; - CollectionControlModel _collectionControl = new CollectionControlModel(); + final flutterWebviewPlugin = new FlutterWebviewPlugin(); final GlobalKey _scaffoldKey = GlobalKey(); + @override void initState() { super.initState(); - _collectionControl - .getRouterByName(Uri.encodeComponent(widget.title.trim())) - .then((list) { - list.forEach((item) { - if (widget.title.trim() == item['name']) { - _router = item['router']; + + flutterWebviewPlugin.onUrlChanged.listen((String url) { + print('url change:$url'); + if (url.indexOf('loginSuccess') > -1) { + String urlQuery = url.substring(url.indexOf('?') + 1); + String loginName, token; + List queryList = urlQuery.split('&'); + for (int i = 0; i < queryList.length; i++) { + String queryNote = queryList[i]; + int eqIndex = queryNote.indexOf('='); + if (queryNote.substring(0, eqIndex) == 'loginName') { + loginName = queryNote.substring(eqIndex + 1); + } + if (queryNote.substring(0, eqIndex) == 'accessToken') { + token = queryNote.substring(eqIndex + 1); + } } - }); - if (mounted) { - setState(() { - _hasCollected = list.length > 0; - }); + if (ApplicationEvent.event != null) { + ApplicationEvent.event + .fire(UserGithubOAuthEvent(loginName, token, true)); + } + print('ready close'); + + flutterWebviewPlugin.close(); + // 验证成功 + } else if (url.indexOf('${Api.BASE_URL}loginFail') == 0) { + // 验证失败 + if (ApplicationEvent.event != null) { + ApplicationEvent.event.fire(UserGithubOAuthEvent('', '', true)); + } + flutterWebviewPlugin.close(); } }); } - // 点击收藏按钮 - _getCollection() { - if (_hasCollected) { - // 删除操作 - _collectionControl - .deleteByName(Uri.encodeComponent(widget.title.trim())) - .then((result) { - if (result > 0 && this.mounted) { - setState(() { - _hasCollected = false; - }); - _scaffoldKey.currentState - .showSnackBar(SnackBar(content: Text('已取消收藏'))); - if (ApplicationEvent.event != null) { - ApplicationEvent.event - .fire(CollectionEvent(widget.title, _router, true)); - } - return; - } - print('删除错误'); - }); - } else { - // 插入操作 - _collectionControl - .insert(Collection( - name: Uri.encodeComponent(widget.title.trim()), - router: widget.url)) - .then((result) { - if (this.mounted) { - setState(() { - _hasCollected = true; - }); - - if (ApplicationEvent.event != null) { - ApplicationEvent.event - .fire(CollectionEvent(widget.title, _router, false)); - } - _scaffoldKey.currentState - .showSnackBar(SnackBar(content: Text('收藏成功'))); - } - }); - } - } - @override Widget build(BuildContext context) { - if (_hasCollected) { - _collectionIcons = Icons.favorite; - } else { - _collectionIcons = Icons.favorite_border; - } return Scaffold( - key: _scaffoldKey, + key: _scaffoldKey, appBar: AppBar( title: Text(widget.title), - actions: [ - new IconButton( - tooltip: 'goBack home', - onPressed: _getCollection, - icon: Icon( - _collectionIcons, - ), - ), - ], ), body: WebviewScaffold( url: widget.url, diff --git a/lib/views/widget_page/widget_page.dart b/lib/views/widget_page/widget_page.dart index dc6b0014..5df042cb 100644 --- a/lib/views/widget_page/widget_page.dart +++ b/lib/views/widget_page/widget_page.dart @@ -6,28 +6,28 @@ import 'package:flutter/material.dart'; import 'package:flutter_go/components/cate_card.dart'; -import 'package:flutter_go/model/cat.dart'; + +import 'package:flutter_go/routers/application.dart'; + class WidgetPage extends StatefulWidget { - final db; - final CatControlModel catModel; - WidgetPage(this.db) - : catModel = new CatControlModel(), - super(); + + + @override - SecondPageState createState() => new SecondPageState(catModel); + SecondPageState createState() => new SecondPageState(); } class SecondPageState extends State with AutomaticKeepAliveClientMixin{ - CatControlModel catModel; - SecondPageState(this.catModel) : super(); + + SecondPageState() : super(); TextEditingController controller; String active = 'test'; String data = '无'; - List categories = []; + @override bool get wantKeepAlive => true; @@ -35,25 +35,16 @@ class SecondPageState extends State with AutomaticKeepAliveClientMix @override void initState() { super.initState(); - renderCats(); } - void renderCats() { - catModel.getList().then((List data) { - if (data.isNotEmpty) { - setState(() { - categories = data; - }); - } - }); - } + Widget buildGrid() { // 存放最后的widget List tiles = []; - for (Cat item in categories) { + Application.widgetTree.children.forEach((dynamic item) { tiles.add(new CateCard(category: item)); - } + }); return new ListView( children: tiles, ); @@ -62,11 +53,6 @@ class SecondPageState extends State with AutomaticKeepAliveClientMix @override Widget build(BuildContext context) { super.build(context); - if (categories.length == 0) { - return ListView( - children: [new Container()], - ); - } return Container( color: Theme.of(context).backgroundColor, child: this.buildGrid(), diff --git a/lib/widgets/components/Navigation/BottomNavigationBar/demo.dart b/lib/widgets/components/Navigation/BottomNavigationBar/demo.dart index 6771263b..62af199c 100644 --- a/lib/widgets/components/Navigation/BottomNavigationBar/demo.dart +++ b/lib/widgets/components/Navigation/BottomNavigationBar/demo.dart @@ -36,6 +36,8 @@ class _BottomNavigationBarFullDefault extends State { currentIndex: _currentIndex, // 当前所高亮的按钮index onTap: _onItemTapped, // 点击里面的按钮的回调函数,参数为当前点击的按钮 index fixedColor: Colors.deepPurple, // 如果 type 类型为 fixed,则通过 fixedColor 设置选中 item 的颜色 + selectedFontSize: 12.0, + unselectedFontSize: 12.0, items: [ BottomNavigationBarItem( title: Text("Home"), icon: Icon(Icons.home)), diff --git a/lib/widgets/components/Navigation/BottomNavigationBar/index.dart b/lib/widgets/components/Navigation/BottomNavigationBar/index.dart index 019ea65b..ae1e72e5 100644 --- a/lib/widgets/components/Navigation/BottomNavigationBar/index.dart +++ b/lib/widgets/components/Navigation/BottomNavigationBar/index.dart @@ -25,6 +25,7 @@ const String _text1 = - 它作为 Scaffold.bottomNavigationBar 参数; - BottomNavigationBar 3-5个之间个底部按钮数量是合理的,理论上 icon 大小合适,可以支持更多; - 默认0-3个底部按钮数量时,BottomNavigationBar采用fixed的模式摆放底部按钮,当有4个时默认使用 BottomNavigationBarType.shifting 模式摆放底部按钮; +- 大多数人可能不需要选中文字被放大,可以通过 设置 selectedFontSize 与 unselectedFontSize 同等大小覆盖默认效果; - 下面的底部导航即是效果; """; diff --git a/lib/widgets/components/Scroll/ScrollMetrics/demo.dart b/lib/widgets/components/Scroll/ScrollMetrics/demo.dart index db2f8aca..5a6a9fc4 100644 --- a/lib/widgets/components/Scroll/ScrollMetrics/demo.dart +++ b/lib/widgets/components/Scroll/ScrollMetrics/demo.dart @@ -27,6 +27,7 @@ class _ScrollMetricsDemoState extends State { }); print("BottomEdge: ${notification.metrics.extentAfter == 0}"); //return true; //放开此行注释后,进度条将失效 + return false; }, child: Stack( alignment: Alignment.center, diff --git a/lib/widgets/components/Scroll/ScrollPhysics/demo.dart b/lib/widgets/components/Scroll/ScrollPhysics/demo.dart index e4208050..3b31a90e 100644 --- a/lib/widgets/components/Scroll/ScrollPhysics/demo.dart +++ b/lib/widgets/components/Scroll/ScrollPhysics/demo.dart @@ -24,6 +24,7 @@ class _ScrollPhysicsDemoState extends State { setState(() { _currentPage = _pageController.page; }); + return false; }, child: PageView.custom( physics: const PageScrollPhysics( diff --git a/lib/widgets/elements/Form/Input/TextField/index.dart b/lib/widgets/elements/Form/Input/TextField/index.dart index fe5fe77b..bbb2900c 100644 --- a/lib/widgets/elements/Form/Input/TextField/index.dart +++ b/lib/widgets/elements/Form/Input/TextField/index.dart @@ -34,7 +34,7 @@ const String _textFieldText2 = """### **进阶用法** """; class Demo extends StatefulWidget { - static const String routeName = 'elements/Form/Input/TextField'; + static const String routeName = 'element/Form/Input/TextField'; @override _DemoState createState() => _DemoState(); diff --git a/lib/widgets/elements/Form/Radio/Radio/index.dart b/lib/widgets/elements/Form/Radio/Radio/index.dart index 8ec9dd9f..ba347f19 100644 --- a/lib/widgets/elements/Form/Radio/Radio/index.dart +++ b/lib/widgets/elements/Form/Radio/Radio/index.dart @@ -6,7 +6,6 @@ /// target: Radio相关 import 'package:flutter/material.dart'; - import 'package:flutter_go/components/widget_demo.dart'; import 'demo.dart'; @@ -26,7 +25,7 @@ Radio widget 代表表单中的单选按钮, 当groupValue = value时代表组 - value → T - 单选的值。 """; class Demo extends StatefulWidget { - static const String routeName = '/element/Form/Radio/index'; + static const String routeName = '/element/Form/Radio/Radio'; _DemoState createState() => _DemoState(); } @@ -40,7 +39,7 @@ class _DemoState extends State { ], title: 'Radio', docUrl: 'https://docs.flutter.io/flutter/material/Radio-class.html', - codeUrl: 'elements/Form/Radio/Radio/index.dart', + codeUrl: 'elements/Form/Radio/Radio/demo.dart', ); } } diff --git a/lib/widgets/elements/Form/Slider/Slider/index.dart b/lib/widgets/elements/Form/Slider/Slider/index.dart index 9862e916..515ae27e 100644 --- a/lib/widgets/elements/Form/Slider/Slider/index.dart +++ b/lib/widgets/elements/Form/Slider/Slider/index.dart @@ -70,7 +70,7 @@ const contentB = ''' '''; class Demo extends StatefulWidget { - static const String routeName = 'elements/Form/Slider/Slider'; + static const String routeName = 'element/form/Slider/Slider'; _Demo createState() => _Demo(); } diff --git a/lib/widgets/elements/Form/Switch/AnimatedSwitcher/index.dart b/lib/widgets/elements/Form/Switch/AnimatedSwitcher/index.dart index 1f90412a..85c42be1 100644 --- a/lib/widgets/elements/Form/Switch/AnimatedSwitcher/index.dart +++ b/lib/widgets/elements/Form/Switch/AnimatedSwitcher/index.dart @@ -34,7 +34,7 @@ class Demo extends StatefulWidget { class _Demo extends State { Widget build(BuildContext context) { return WidgetDemo( - title: 'SwitchListTile', + title: 'AnimatedSwitcher', codeUrl: 'elements/Form/Switch/AnimatedSwitcher/demo.dart', contentList: [contentA, AnimatedSwitcherDemo()], docUrl: '', diff --git a/lib/widgets/elements/Form/Text/RichText/index.dart b/lib/widgets/elements/Form/Text/RichText/index.dart index b5bb5be8..0e2e8451 100644 --- a/lib/widgets/elements/Form/Text/RichText/index.dart +++ b/lib/widgets/elements/Form/Text/RichText/index.dart @@ -30,7 +30,7 @@ class _Demo extends State { return WidgetDemo( title: 'Rich Text', docUrl: 'https://docs.flutter.io/flutter/widgets/RichText-class.html', - codeUrl: 'elements/Form/Text/RichText/index.dart', + codeUrl: 'elements/Form/Text/RichText/demo.dart', contentList: [ intro, RichTextDemo(), diff --git a/lib/widgets/themes/Cupertino/CupertinoApp/demo.dart b/lib/widgets/themes/Cupertino/CupertinoApp/demo.dart index f503023b..ddd7888a 100644 --- a/lib/widgets/themes/Cupertino/CupertinoApp/demo.dart +++ b/lib/widgets/themes/Cupertino/CupertinoApp/demo.dart @@ -95,16 +95,16 @@ class _CupertinoAppFullDefault extends State { ], onGenerateRoute: (setting) { // 当通过Navigation.of(context).pushNamed跳转路由时,在routes查找不到时,会调用该方法 - routerHandler(setting); + return routerHandler(setting); }, onGenerateTitle: (context) { // 跟上面的tiitle一样,但含有一个context参数用于做本地化 return 'Flutter应用'; }, - onUnknownRoute: (setting) { - // 效果跟onGenerateRoute一样调用顺序为onGenerateRoute ==> onUnknownRoute - //RouterHandler(setting); - }, +// onUnknownRoute: (setting) { +// // 效果跟onGenerateRoute一样调用顺序为onGenerateRoute ==> onUnknownRoute +// //RouterHandler(setting); +// }, routes: { // 声明程序中有哪个通过Navigation.of(context).pushNamed跳转的路由参数以键值对的形式传递key:路由名字value:对应的Widget '/home': (BuildContext context) => HomePage(), diff --git a/lib/widgets/themes/Cupertino/CupertinoSliverNavigationBar/demo.dart b/lib/widgets/themes/Cupertino/CupertinoSliverNavigationBar/demo.dart index bbb27bd5..5e272294 100644 --- a/lib/widgets/themes/Cupertino/CupertinoSliverNavigationBar/demo.dart +++ b/lib/widgets/themes/Cupertino/CupertinoSliverNavigationBar/demo.dart @@ -29,8 +29,9 @@ class _Demo extends State { trailing: Icon(Icons.perm_camera_mic) ), CupertinoSliverRefreshControl( - onRefresh: () { - }, +// onRefresh: () { +// +// }, ), SliverSafeArea( top: false, // Top safe area is consumed by the navigation bar. diff --git a/lib/widgets/themes/Material/MaterialColor/index.dart b/lib/widgets/themes/Material/MaterialColor/index.dart index d4467e98..79117f93 100644 --- a/lib/widgets/themes/Material/MaterialColor/index.dart +++ b/lib/widgets/themes/Material/MaterialColor/index.dart @@ -21,7 +21,7 @@ const String content1 = ''' '''; class Demo extends StatefulWidget { - static const String routeName = '/themes/Material/MaterialColor'; + static const String routeName = '/Themes/Material/MaterialColor'; _DemoState createState() => _DemoState(); } diff --git a/pubspec.yaml b/pubspec.yaml index 3362f571..1aa90e90 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: flutter_go version: 1.0.0 environment: - sdk: ">=2.0.0-dev.68.0 <3.0.0" + sdk: ">=2.1.0 <3.0.0" dependencies: flutter: @@ -22,16 +22,21 @@ dependencies: cupertino_icons: ^0.1.2 event_bus: ^1.0.1 fluro: ^1.3.4 - image_picker: ^0.6.0+1 + image_picker: ^0.5.0 sqflite: ^1.1.5 +<<<<<<< HEAD flutter_markdown: ^0.2.0 +======= +>>>>>>> dxj/master url_launcher: ^5.0.2 # 本地存储、收藏功能 shared_preferences: ^0.4.3 + share: ^0.6.1+1 flutter_spinkit: "^3.1.0" + fluttertoast: ^3.1.0 dio: ^2.0.15 - flutter_webview_plugin: ^0.3.4 + flutter_webview_plugin: ^0.3.5 cookie_jar: ^1.0.0 # 日期格式化 intl: 0.15.7 @@ -42,11 +47,20 @@ dependencies: flutter_bloc: ^0.11.1 bloc: ^0.12.0 html: ^0.14.0+2 + markdown: ^2.0.0 + meta: ^1.0.5 + string_scanner: ^1.0.0 + path: ^1.5.1 flutter_downloader: ^1.1.7 path_provider: ^1.1.0 permission_handler: ^3.0.0 open_file: ^2.0.1+2 package_info: ^0.4.0+3 + flutter_jpush: ^0.0.4 + zefyr: + path: ./zefyr + pull_to_refresh: ^1.5.6 + dev_dependencies: flutter_test: @@ -54,7 +68,6 @@ dev_dependencies: # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec - # The following section is specific to Flutter. flutter: @@ -205,6 +218,8 @@ flutter: - lib/widgets/themes/Cupertino/CupertinoTabScaffold/demo.dart - lib/widgets/themes/Cupertino/CupertinoTabView/demo.dart - lib/widgets/themes/Cupertino/CupertinoTimerPicker/demo.dart + - lib/page_demo_package/.demo.json + - lib/standard_pages/.pages.json - assets/app.db - assets/images/ - assets/fonts/ diff --git a/zefyr/.gitignore b/zefyr/.gitignore new file mode 100644 index 00000000..9f87252c --- /dev/null +++ b/zefyr/.gitignore @@ -0,0 +1,72 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/zefyr/CHANGELOG.md b/zefyr/CHANGELOG.md new file mode 100644 index 00000000..ac071598 --- /dev/null +++ b/zefyr/CHANGELOG.md @@ -0,0 +1,3 @@ +## [0.0.1] - TODO: Add release date. + +* TODO: Describe initial release. diff --git a/zefyr/LICENSE b/zefyr/LICENSE new file mode 100644 index 00000000..ba75c69f --- /dev/null +++ b/zefyr/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/zefyr/README.md b/zefyr/README.md new file mode 100644 index 00000000..cc0c4567 --- /dev/null +++ b/zefyr/README.md @@ -0,0 +1,14 @@ +# zefyr + +A new Flutter package project. + +## Getting Started + +This project is a starting point for a Dart +[package](https://flutter.dev/developing-packages/), +a library module containing code that can be shared easily across +multiple Flutter or Dart projects. + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/zefyr/lib/src/fast_diff.dart b/zefyr/lib/src/fast_diff.dart new file mode 100644 index 00000000..ade850b6 --- /dev/null +++ b/zefyr/lib/src/fast_diff.dart @@ -0,0 +1,40 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'dart:math' as math; + +/// Performs a fast diff operation on two input strings based on provided +/// [cursorPosition]. +DiffResult fastDiff(String oldText, String newText, int cursorPosition) { + var delta = newText.length - oldText.length; + var limit = math.max(0, cursorPosition - delta); + var end = oldText.length; + while (end > limit && oldText[end - 1] == newText[end + delta - 1]) { + end -= 1; + } + var start = 0; + var startLimit = cursorPosition - math.max(0, delta); + while (start < startLimit && oldText[start] == newText[start]) { + start += 1; + } + final String deleted = (start < end) ? oldText.substring(start, end) : ''; + final inserted = newText.substring(start, end + delta); + return new DiffResult(start, deleted, inserted); +} + +/// A diff between two strings of text. +class DiffResult { + /// Start index in old text at which changes begin. + final int start; + + /// Deleted text in old text. + final String deleted; + + /// Inserted text. + final String inserted; + + DiffResult(this.start, this.deleted, this.inserted); + + @override + String toString() => 'DiffResult[$start, "$deleted", "$inserted"]'; +} diff --git a/zefyr/lib/src/widgets/buttons.dart b/zefyr/lib/src/widgets/buttons.dart new file mode 100644 index 00000000..675f4ea3 --- /dev/null +++ b/zefyr/lib/src/widgets/buttons.dart @@ -0,0 +1,583 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:notus/notus.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'scope.dart'; +import 'theme.dart'; +import 'toolbar.dart'; + +/// A button used in [ZefyrToolbar]. +/// +/// Create an instance of this widget with [ZefyrButton.icon] or +/// [ZefyrButton.text] constructors. +/// +/// Toolbar buttons are normally created by a [ZefyrToolbarDelegate]. +class ZefyrButton extends StatelessWidget { + /// Creates a toolbar button with an icon. + ZefyrButton.icon({ + @required this.action, + @required IconData icon, + double iconSize, + this.onPressed, + }) : assert(action != null), + assert(icon != null), + _icon = icon, + _iconSize = iconSize, + _text = null, + _textStyle = null, + super(); + + /// Creates a toolbar button containing text. + /// + /// Note that [ZefyrButton] has fixed width and does not expand to accommodate + /// long texts. + ZefyrButton.text({ + @required this.action, + @required String text, + TextStyle style, + this.onPressed, + }) : assert(action != null), + assert(text != null), + _icon = null, + _iconSize = null, + _text = text, + _textStyle = style, + super(); + + /// Toolbar action associated with this button. + final ZefyrToolbarAction action; + final IconData _icon; + final double _iconSize; + final String _text; + final TextStyle _textStyle; + + /// Callback to trigger when this button is tapped. + final VoidCallback onPressed; + + bool get isAttributeAction { + return kZefyrToolbarAttributeActions.keys.contains(action); + } + + @override + Widget build(BuildContext context) { + final toolbar = ZefyrToolbar.of(context); + final editor = toolbar.editor; + final toolbarTheme = ZefyrTheme.of(context).toolbarTheme; + final pressedHandler = _getPressedHandler(editor, toolbar); + final iconColor = (pressedHandler == null) + ? toolbarTheme.disabledIconColor + : toolbarTheme.iconColor; + if (_icon != null) { + return RawZefyrButton.icon( + action: action, + icon: _icon, + size: _iconSize, + iconColor: iconColor, + color: _getColor(editor, toolbarTheme), + onPressed: _getPressedHandler(editor, toolbar), + ); + } else { + assert(_text != null); + var style = _textStyle ?? new TextStyle(); + style = style.copyWith(color: iconColor); + return RawZefyrButton( + action: action, + child: new Text(_text, style: style), + color: _getColor(editor, toolbarTheme), + onPressed: _getPressedHandler(editor, toolbar), + ); + } + } + + Color _getColor(ZefyrScope editor, ZefyrToolbarTheme theme) { + if (isAttributeAction) { + final attribute = kZefyrToolbarAttributeActions[action]; + final isToggled = (attribute is NotusAttribute) + ? editor.selectionStyle.containsSame(attribute) + : editor.selectionStyle.contains(attribute); + return isToggled ? theme.toggleColor : null; + } + return null; + } + + VoidCallback _getPressedHandler( + ZefyrScope editor, ZefyrToolbarState toolbar) { + if (onPressed != null) { + return onPressed; + } else if (isAttributeAction) { + final attribute = kZefyrToolbarAttributeActions[action]; + if (attribute is NotusAttribute) { + return () => _toggleAttribute(attribute, editor); + } + } else if (action == ZefyrToolbarAction.close) { + return () => toolbar.closeOverlay(); + } else if (action == ZefyrToolbarAction.hideKeyboard) { + return () => editor.hideKeyboard(); + } + + return null; + } + + void _toggleAttribute(NotusAttribute attribute, ZefyrScope editor) { + final isToggled = editor.selectionStyle.containsSame(attribute); + if (isToggled) { + editor.formatSelection(attribute.unset); + } else { + editor.formatSelection(attribute); + } + } +} + +/// Raw button widget used by [ZefyrToolbar]. +/// +/// See also: +/// +/// * [ZefyrButton], which wraps this widget and implements most of the +/// action-specific logic. +class RawZefyrButton extends StatelessWidget { + const RawZefyrButton({ + Key key, + @required this.action, + @required this.child, + @required this.color, + @required this.onPressed, + }) : super(key: key); + + /// Creates a [RawZefyrButton] containing an icon. + RawZefyrButton.icon({ + @required this.action, + @required IconData icon, + double size, + Color iconColor, + @required this.color, + @required this.onPressed, + }) : child = new Icon(icon, size: size, color: iconColor), + super(); + + /// Toolbar action associated with this button. + final ZefyrToolbarAction action; + + /// Child widget to show inside this button. Usually an icon. + final Widget child; + + /// Background color of this button. + final Color color; + + /// Callback to trigger when this button is pressed. + final VoidCallback onPressed; + + /// Returns `true` if this button is currently toggled on. + bool get isToggled => color != null; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final width = theme.buttonTheme.constraints.minHeight + 4.0; + final constraints = theme.buttonTheme.constraints.copyWith( + minWidth: width, maxHeight: theme.buttonTheme.constraints.minHeight); + final radius = BorderRadius.all(Radius.circular(3.0)); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 1.0, vertical: 6.0), + child: RawMaterialButton( + shape: RoundedRectangleBorder(borderRadius: radius), + elevation: 0.0, + fillColor: color, + constraints: constraints, + onPressed: onPressed, + child: child, + ), + ); + } +} + +/// Controls heading styles. +/// +/// When pressed, this button displays overlay toolbar with three +/// buttons for each heading level. +class HeadingButton extends StatefulWidget { + const HeadingButton({Key key}) : super(key: key); + + @override + _HeadingButtonState createState() => _HeadingButtonState(); +} + +class _HeadingButtonState extends State { + @override + Widget build(BuildContext context) { + final toolbar = ZefyrToolbar.of(context); + return toolbar.buildButton( + context, + ZefyrToolbarAction.heading, + onPressed: showOverlay, + ); + } + + void showOverlay() { + final toolbar = ZefyrToolbar.of(context); + toolbar.showOverlay(buildOverlay); + } + + Widget buildOverlay(BuildContext context) { + final toolbar = ZefyrToolbar.of(context); + final buttons = Row( + children: [ + SizedBox(width: 8.0), + toolbar.buildButton(context, ZefyrToolbarAction.headingLevel1), + toolbar.buildButton(context, ZefyrToolbarAction.headingLevel2), + toolbar.buildButton(context, ZefyrToolbarAction.headingLevel3), + ], + ); + return ZefyrToolbarScaffold(body: buttons); + } +} + +/// Controls image attribute. +/// +/// When pressed, this button displays overlay toolbar with three +/// buttons for each heading level. +class ImageButton extends StatefulWidget { + const ImageButton({Key key}) : super(key: key); + + @override + _ImageButtonState createState() => _ImageButtonState(); +} + +class _ImageButtonState extends State { + @override + Widget build(BuildContext context) { + final toolbar = ZefyrToolbar.of(context); + return toolbar.buildButton( + context, + ZefyrToolbarAction.image, + onPressed: showOverlay, + ); + } + + void showOverlay() { + final toolbar = ZefyrToolbar.of(context); + toolbar.showOverlay(buildOverlay); + } + + Widget buildOverlay(BuildContext context) { + final toolbar = ZefyrToolbar.of(context); + final buttons = Row( + children: [ + SizedBox(width: 8.0), + toolbar.buildButton(context, ZefyrToolbarAction.cameraImage, + onPressed: _pickFromCamera), + toolbar.buildButton(context, ZefyrToolbarAction.galleryImage, + onPressed: _pickFromGallery), + ], + ); + return ZefyrToolbarScaffold(body: buttons); + } + + void _pickFromCamera() async { + final editor = ZefyrToolbar.of(context).editor; + final image = await editor.imageDelegate.pickImage(ImageSource.camera); + if (image != null) + editor.formatSelection(NotusAttribute.embed.image(image)); + } + + void _pickFromGallery() async { + final editor = ZefyrToolbar.of(context).editor; + final image = await editor.imageDelegate.pickImage(ImageSource.gallery); + if (image != null) + editor.formatSelection(NotusAttribute.embed.image(image)); + } +} + +class LinkButton extends StatefulWidget { + const LinkButton({Key key}) : super(key: key); + + @override + _LinkButtonState createState() => _LinkButtonState(); +} + +class _LinkButtonState extends State { + final TextEditingController _inputController = TextEditingController(); + Key _inputKey; + bool _formatError = false; + ZefyrScope _editor; + + bool get isEditing => _inputKey != null; + + @override + Widget build(BuildContext context) { + final toolbar = ZefyrToolbar.of(context); + final editor = toolbar.editor; + final enabled = + hasLink(editor.selectionStyle) || !editor.selection.isCollapsed; + + return toolbar.buildButton( + context, + ZefyrToolbarAction.link, + onPressed: enabled ? showOverlay : null, + ); + } + + bool hasLink(NotusStyle style) => style.contains(NotusAttribute.link); + + String getLink([String defaultValue]) { + final editor = ZefyrToolbar.of(context).editor; + final attrs = editor.selectionStyle; + if (hasLink(attrs)) { + return attrs.value(NotusAttribute.link); + } + return defaultValue; + } + + void showOverlay() { + final toolbar = ZefyrToolbar.of(context); + toolbar.showOverlay(buildOverlay).whenComplete(cancelEdit); + } + + void closeOverlay() { + final toolbar = ZefyrToolbar.of(context); + toolbar.closeOverlay(); + } + + void edit() { + final toolbar = ZefyrToolbar.of(context); + setState(() { + _inputKey = new UniqueKey(); + _inputController.text = getLink('https://'); + _inputController.addListener(_handleInputChange); + toolbar.markNeedsRebuild(); + }); + } + + void doneEdit() { + final toolbar = ZefyrToolbar.of(context); + setState(() { + var error = false; + if (_inputController.text.isNotEmpty) { + try { + var uri = Uri.parse(_inputController.text); + if ((uri.isScheme('https') || uri.isScheme('http')) && + uri.host.isNotEmpty) { + toolbar.editor.formatSelection( + NotusAttribute.link.fromString(_inputController.text)); + } else { + error = true; + } + } on FormatException { + error = true; + } + } + if (error) { + _formatError = error; + toolbar.markNeedsRebuild(); + } else { + _inputKey = null; + _inputController.text = ''; + _inputController.removeListener(_handleInputChange); + toolbar.markNeedsRebuild(); + toolbar.editor.focus(); + } + }); + } + + void cancelEdit() { + if (mounted) { + final editor = ZefyrToolbar.of(context).editor; + setState(() { + _inputKey = null; + _inputController.text = ''; + _inputController.removeListener(_handleInputChange); + editor.focus(); + }); + } + } + + void unlink() { + final editor = ZefyrToolbar.of(context).editor; + editor.formatSelection(NotusAttribute.link.unset); + closeOverlay(); + } + + void copyToClipboard() { + var link = getLink(); + assert(link != null); + Clipboard.setData(new ClipboardData(text: link)); + } + + void openInBrowser() async { + final editor = ZefyrToolbar.of(context).editor; + var link = getLink(); + assert(link != null); + if (await canLaunch(link)) { + editor.hideKeyboard(); + await launch(link, forceWebView: true); + } + } + + void _handleInputChange() { + final toolbar = ZefyrToolbar.of(context); + setState(() { + _formatError = false; + toolbar.markNeedsRebuild(); + }); + } + + Widget buildOverlay(BuildContext context) { + final toolbar = ZefyrToolbar.of(context); + final style = toolbar.editor.selectionStyle; + + String value = 'Tap to edit link'; + if (style.contains(NotusAttribute.link)) { + value = style.value(NotusAttribute.link); + } + final clipboardEnabled = value != 'Tap to edit link'; + final body = !isEditing + ? _LinkView(value: value, onTap: edit) + : _LinkInput( + key: _inputKey, + controller: _inputController, + formatError: _formatError, + ); + final items = [Expanded(child: body)]; + if (!isEditing) { + final unlinkHandler = hasLink(style) ? unlink : null; + final copyHandler = clipboardEnabled ? copyToClipboard : null; + final openHandler = hasLink(style) ? openInBrowser : null; + final buttons = [ + toolbar.buildButton(context, ZefyrToolbarAction.unlink, + onPressed: unlinkHandler), + toolbar.buildButton(context, ZefyrToolbarAction.clipboardCopy, + onPressed: copyHandler), + toolbar.buildButton( + context, + ZefyrToolbarAction.openInBrowser, + onPressed: openHandler, + ), + ]; + items.addAll(buttons); + } + final trailingPressed = isEditing ? doneEdit : closeOverlay; + final trailingAction = + isEditing ? ZefyrToolbarAction.confirm : ZefyrToolbarAction.close; + + return ZefyrToolbarScaffold( + body: Row(children: items), + trailing: toolbar.buildButton( + context, + trailingAction, + onPressed: trailingPressed, + ), + ); + } +} + +class _LinkInput extends StatefulWidget { + final TextEditingController controller; + final bool formatError; + + const _LinkInput( + {Key key, @required this.controller, this.formatError: false}) + : super(key: key); + + @override + _LinkInputState createState() { + return new _LinkInputState(); + } +} + +class _LinkInputState extends State<_LinkInput> { + final FocusNode _focusNode = FocusNode(); + + ZefyrScope _editor; + bool _didAutoFocus = false; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (!_didAutoFocus) { + FocusScope.of(context).requestFocus(_focusNode); + _didAutoFocus = true; + } + + final toolbar = ZefyrToolbar.of(context); + + if (_editor != toolbar.editor) { + _editor?.toolbarFocusNode = null; + _editor = toolbar.editor; + _editor.toolbarFocusNode = _focusNode; + } + } + + @override + void dispose() { + _editor?.toolbarFocusNode = null; + _focusNode.dispose(); + _editor = null; + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final toolbarTheme = ZefyrTheme.of(context).toolbarTheme; + final color = + widget.formatError ? Colors.redAccent : toolbarTheme.iconColor; + final style = theme.textTheme.subhead.copyWith(color: color); + return TextField( + style: style, + keyboardType: TextInputType.url, + focusNode: _focusNode, + controller: widget.controller, + autofocus: true, + decoration: new InputDecoration( + hintText: 'https://', + filled: true, + fillColor: toolbarTheme.color, + border: InputBorder.none, + contentPadding: const EdgeInsets.all(10.0), + ), + ); + } +} + +class _LinkView extends StatelessWidget { + const _LinkView({Key key, @required this.value, this.onTap}) + : super(key: key); + final String value; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final toolbarTheme = ZefyrTheme.of(context).toolbarTheme; + Widget widget = new ClipRect( + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: ZefyrToolbar.kToolbarHeight), + padding: const EdgeInsets.all(10.0), + child: Text( + value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.subhead + .copyWith(color: toolbarTheme.disabledIconColor), + ), + ) + ], + ), + ); + if (onTap != null) { + widget = GestureDetector( + child: widget, + onTap: onTap, + ); + } + return widget; + } +} diff --git a/zefyr/lib/src/widgets/caret.dart b/zefyr/lib/src/widgets/caret.dart new file mode 100644 index 00000000..895cd3b4 --- /dev/null +++ b/zefyr/lib/src/widgets/caret.dart @@ -0,0 +1,42 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +/// Helper class responsible for cursor layout and painting. +class CursorPainter { + static const double _kCaretHeightOffset = 2.0; // pixels + static const double _kCaretWidth = 1.0; // pixels + + static Rect buildPrototype(double lineHeight) { + return new Rect.fromLTWH( + 0.0, 0.0, _kCaretWidth, lineHeight - _kCaretHeightOffset); + } + + CursorPainter(Color color) + : assert(color != null), + _color = color; + + Rect _prototype; + + Rect get prototype => _prototype; + + Color _color; + Color get color => _color; + set color(Color value) { + assert(value != null); + _color = value; + } + + void layout(double lineHeight) { + _prototype = buildPrototype(lineHeight); + } + + void paint(Canvas canvas, Offset offset) { + final Paint paint = new Paint()..color = _color; + final Rect caretRect = _prototype.shift(offset); + canvas.drawRect(caretRect, paint); + } +} diff --git a/zefyr/lib/src/widgets/code.dart b/zefyr/lib/src/widgets/code.dart new file mode 100644 index 00000000..1c5c6050 --- /dev/null +++ b/zefyr/lib/src/widgets/code.dart @@ -0,0 +1,47 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'package:flutter/material.dart'; +import 'package:notus/notus.dart'; + +import 'common.dart'; +import 'theme.dart'; + +/// Represents a code snippet in Zefyr editor. +class ZefyrCode extends StatelessWidget { + const ZefyrCode({Key key, @required this.node}) : super(key: key); + + /// Document node represented by this widget. + final BlockNode node; + + @override + Widget build(BuildContext context) { + final theme = ZefyrTheme.of(context); + + List items = []; + for (var line in node.children) { + items.add(_buildLine(line, theme.blockTheme.code.textStyle)); + } + + return new Padding( + padding: theme.blockTheme.code.padding, + child: new Container( + // TODO: make decorations configurable + decoration: BoxDecoration( + color: Colors.blueGrey.shade50, + borderRadius: BorderRadius.circular(3.0), + ), + padding: const EdgeInsets.all(16.0), + child: new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: items, + ), + ), + ); + } + + Widget _buildLine(Node node, TextStyle style) { + LineNode line = node; + return new RawZefyrLine(node: line, style: style); + } +} diff --git a/zefyr/lib/src/widgets/common.dart b/zefyr/lib/src/widgets/common.dart new file mode 100644 index 00000000..aabc8b8f --- /dev/null +++ b/zefyr/lib/src/widgets/common.dart @@ -0,0 +1,155 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:notus/notus.dart'; + +import 'editable_box.dart'; +import 'horizontal_rule.dart'; +import 'image.dart'; +import 'rich_text.dart'; +import 'scope.dart'; +import 'theme.dart'; + +/// Raw widget representing a single line of rich text document in Zefyr editor. +/// +/// See [ZefyrParagraph] and [ZefyrHeading] which wrap this widget and +/// integrate it with current [ZefyrTheme]. +class RawZefyrLine extends StatefulWidget { + const RawZefyrLine({ + Key key, + @required this.node, + this.style, + this.padding, + }) : super(key: key); + + /// Line in the document represented by this widget. + final LineNode node; + + /// Style to apply to this line. Required for lines with text contents, + /// ignored for lines containing embeds. + final TextStyle style; + + /// Padding to add around this paragraph. + final EdgeInsets padding; + + @override + _RawZefyrLineState createState() => new _RawZefyrLineState(); +} + +class _RawZefyrLineState extends State { + final LayerLink _link = new LayerLink(); + + @override + Widget build(BuildContext context) { + final scope = ZefyrScope.of(context); + if (scope.isEditable) { + ensureVisible(context, scope); + } + final theme = ZefyrTheme.of(context); + + Widget content; + if (widget.node.hasEmbed) { + content = buildEmbed(context, scope); + } else { + assert(widget.style != null); + content = ZefyrRichText( + node: widget.node, + text: buildText(context), + ); + } + + if (scope.isEditable) { + content = EditableBox( + child: content, + node: widget.node, + layerLink: _link, + renderContext: scope.renderContext, + showCursor: scope.showCursor, + selection: scope.selection, + selectionColor: theme.selectionColor, + cursorColor: theme.cursorColor, + ); + content = CompositedTransformTarget(link: _link, child: content); + } + + if (widget.padding != null) { + return Padding(padding: widget.padding, child: content); + } + return content; + } + + void ensureVisible(BuildContext context, ZefyrScope scope) { + if (scope.selection.isCollapsed && + widget.node.containsOffset(scope.selection.extentOffset)) { + WidgetsBinding.instance.addPostFrameCallback((_) { + bringIntoView(context); + }); + } + } + + void bringIntoView(BuildContext context) { + ScrollableState scrollable = Scrollable.of(context); + final object = context.findRenderObject(); + assert(object.attached); + final RenderAbstractViewport viewport = RenderAbstractViewport.of(object); + assert(viewport != null); + + final double offset = scrollable.position.pixels; + double target = viewport.getOffsetToReveal(object, 0.0).offset; + if (target - offset < 0.0) { + scrollable.position.jumpTo(target); + return; + } + target = viewport.getOffsetToReveal(object, 1.0).offset; + if (target - offset > 0.0) { + scrollable.position.jumpTo(target); + } + } + + TextSpan buildText(BuildContext context) { + final theme = ZefyrTheme.of(context); + final List children = widget.node.children + .map((node) => _segmentToTextSpan(node, theme)) + .toList(growable: false); + return new TextSpan(style: widget.style, children: children); + } + + TextSpan _segmentToTextSpan(Node node, ZefyrThemeData theme) { + final TextNode segment = node; + final attrs = segment.style; + + return new TextSpan( + text: segment.value, + style: _getTextStyle(attrs, theme), + ); + } + + TextStyle _getTextStyle(NotusStyle style, ZefyrThemeData theme) { + TextStyle result = new TextStyle(); + if (style.containsSame(NotusAttribute.bold)) { + result = result.merge(theme.boldStyle); + } + if (style.containsSame(NotusAttribute.italic)) { + result = result.merge(theme.italicStyle); + } + if (style.contains(NotusAttribute.link)) { + result = result.merge(theme.linkStyle); + } + return result; + } + + Widget buildEmbed(BuildContext context, ZefyrScope scope) { + EmbedNode node = widget.node.children.single; + EmbedAttribute embed = node.style.get(NotusAttribute.embed); + + if (embed.type == EmbedType.horizontalRule) { + return ZefyrHorizontalRule(node: node); + } else if (embed.type == EmbedType.image) { + return ZefyrImage(node: node, delegate: scope.imageDelegate); + } else { + throw new UnimplementedError('Unimplemented embed type ${embed.type}'); + } + } +} diff --git a/zefyr/lib/src/widgets/controller.dart b/zefyr/lib/src/widgets/controller.dart new file mode 100644 index 00000000..325769d3 --- /dev/null +++ b/zefyr/lib/src/widgets/controller.dart @@ -0,0 +1,177 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'dart:math' as math; + +import 'package:flutter/widgets.dart'; +import 'package:notus/notus.dart'; +import 'package:quill_delta/quill_delta.dart'; +import 'package:zefyr/util.dart'; + +const TextSelection _kZeroSelection = const TextSelection.collapsed( + offset: 0, + affinity: TextAffinity.upstream, +); + +/// Owner of focus. +enum FocusOwner { + /// Current owner is the editor. + editor, + + /// Current owner is the toolbar. + toolbar, + + /// No focus owner. + none, +} + +/// Controls instance of [ZefyrEditor]. +class ZefyrController extends ChangeNotifier { + ZefyrController(NotusDocument document) + : assert(document != null), + _document = document; + + /// Zefyr document managed by this controller. + NotusDocument get document => _document; + NotusDocument _document; + + /// Currently selected text within the [document]. + TextSelection get selection => _selection; + TextSelection _selection = _kZeroSelection; + + ChangeSource _lastChangeSource; + + /// Source of the last text or selection change. + ChangeSource get lastChangeSource => _lastChangeSource; + + /// Updates selection with specified [value]. + /// + /// [value] and [source] cannot be `null`. + void updateSelection(TextSelection value, + {ChangeSource source: ChangeSource.remote}) { + _updateSelectionSilent(value, source: source); + notifyListeners(); + } + + // Updates selection without triggering notifications to listeners. + void _updateSelectionSilent(TextSelection value, + {ChangeSource source: ChangeSource.remote}) { + assert(value != null && source != null); + _selection = value; + _lastChangeSource = source; + _ensureSelectionBeforeLastBreak(); + } + + @override + void dispose() { + _document.close(); + super.dispose(); + } + + /// Composes [change] into document managed by this controller. + /// + /// This method does not apply any adjustments or heuristic rules to + /// provided [change] and it is caller's responsibility to ensure this change + /// can be composed without errors. + /// + /// If composing this change fails then this method throws [ComposeError]. + void compose(Delta change, + {TextSelection selection, ChangeSource source: ChangeSource.remote}) { + if (change.isNotEmpty) { + _document.compose(change, source); + } + if (selection != null) { + _updateSelectionSilent(selection, source: source); + } else { + // Transform selection against the composed change and give priority to + // current position (force: false). + final base = + change.transformPosition(_selection.baseOffset, force: false); + final extent = + change.transformPosition(_selection.extentOffset, force: false); + selection = _selection.copyWith(baseOffset: base, extentOffset: extent); + if (_selection != selection) { + _updateSelectionSilent(selection, source: source); + } + } + _lastChangeSource = source; + notifyListeners(); + } + + void replaceText(int index, int length, String text, + {TextSelection selection}) { + Delta delta; + + if (length > 0 || text.isNotEmpty) { + delta = document.replace(index, length, text); + } + + if (selection != null) { + if (delta == null) { + _updateSelectionSilent(selection, source: ChangeSource.local); + } else { + // need to transform selection position in case actual delta + // is different from user's version (in deletes and inserts). + Delta user = new Delta() + ..retain(index) + ..insert(text) + ..delete(length); + int positionDelta = getPositionDelta(user, delta); + _updateSelectionSilent( + selection.copyWith( + baseOffset: selection.baseOffset + positionDelta, + extentOffset: selection.extentOffset + positionDelta, + ), + source: ChangeSource.local, + ); + } + } + _lastChangeSource = ChangeSource.local; + notifyListeners(); + } + + void formatText(int index, int length, NotusAttribute attribute) { + final change = document.format(index, length, attribute); + _lastChangeSource = ChangeSource.local; + // Transform selection against the composed change and give priority to + // the change. This is needed in cases when format operation actually + // inserts data into the document (e.g. embeds). + final base = change.transformPosition(_selection.baseOffset); + final extent = + change.transformPosition(_selection.extentOffset); + final adjustedSelection = + _selection.copyWith(baseOffset: base, extentOffset: extent); + if (_selection != adjustedSelection) { + _updateSelectionSilent(adjustedSelection, source: _lastChangeSource); + } + notifyListeners(); + } + + /// Formats current selection with [attribute]. + void formatSelection(NotusAttribute attribute) { + int index = _selection.start; + int length = _selection.end - index; + formatText(index, length, attribute); + } + + NotusStyle getSelectionStyle() { + int start = _selection.start; + int length = _selection.end - start; + return _document.collectStyle(start, length); + } + + TextEditingValue get plainTextEditingValue { + return new TextEditingValue( + text: document.toPlainText(), + selection: selection, + composing: new TextRange.collapsed(0), + ); + } + + void _ensureSelectionBeforeLastBreak() { + final end = _document.length - 1; + final base = math.min(_selection.baseOffset, end); + final extent = math.min(_selection.extentOffset, end); + _selection = _selection.copyWith(baseOffset: base, extentOffset: extent); + } +} diff --git a/zefyr/lib/src/widgets/cursor_timer.dart b/zefyr/lib/src/widgets/cursor_timer.dart new file mode 100644 index 00000000..947dc5ab --- /dev/null +++ b/zefyr/lib/src/widgets/cursor_timer.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +/// Helper class that keeps state relevant to the editing cursor. +class CursorTimer { + static const _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500); + + Timer _timer; + final ValueNotifier _showCursor = new ValueNotifier(false); + + ValueNotifier get value => _showCursor; + + void _cursorTick(Timer timer) { + _showCursor.value = !_showCursor.value; + } + + /// Starts cursor timer. + void start() { + _showCursor.value = true; + _timer = new Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick); + } + + /// Stops cursor timer. + void stop() { + _timer?.cancel(); + _timer = null; + _showCursor.value = false; + } + + /// Starts or stops cursor timer based on current state of [focusNode] + /// and [selection]. + void startOrStop(FocusNode focusNode, TextSelection selection) { + final hasFocus = focusNode.hasFocus; + final selectionCollapsed = selection.isCollapsed; + if (_timer == null && hasFocus && selectionCollapsed) { + start(); + } else if (_timer != null && (!hasFocus || !selectionCollapsed)) { + stop(); + } + } +} diff --git a/zefyr/lib/src/widgets/editable_box.dart b/zefyr/lib/src/widgets/editable_box.dart new file mode 100644 index 00000000..71c52f48 --- /dev/null +++ b/zefyr/lib/src/widgets/editable_box.dart @@ -0,0 +1,341 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'dart:math' as math; +import 'dart:ui' as ui; + +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:notus/notus.dart'; + +import 'caret.dart'; +import 'render_context.dart'; + +class EditableBox extends SingleChildRenderObjectWidget { + EditableBox({ + @required Widget child, + @required this.node, + @required this.layerLink, + @required this.renderContext, + @required this.showCursor, + @required this.selection, + @required this.selectionColor, + @required this.cursorColor, + }) : super(child: child); + + final ContainerNode node; + final LayerLink layerLink; + final ZefyrRenderContext renderContext; + final ValueNotifier showCursor; + final TextSelection selection; + final Color selectionColor; + final Color cursorColor; + + @override + RenderEditableProxyBox createRenderObject(BuildContext context) { + return new RenderEditableProxyBox( + node: node, + layerLink: layerLink, + renderContext: renderContext, + showCursor: showCursor, + selection: selection, + selectionColor: selectionColor, + cursorColor: cursorColor, + ); + } + + @override + void updateRenderObject( + BuildContext context, RenderEditableProxyBox renderObject) { + renderObject + ..node = node + ..layerLink = layerLink + ..renderContext = renderContext + ..showCursor = showCursor + ..selection = selection + ..selectionColor = selectionColor + ..cursorColor = cursorColor; + } +} + +class RenderEditableProxyBox extends RenderBox + with + RenderObjectWithChildMixin, + RenderProxyBoxMixin + implements RenderEditableBox { + RenderEditableProxyBox({ + RenderEditableBox child, + @required ContainerNode node, + @required LayerLink layerLink, + @required ZefyrRenderContext renderContext, + @required ValueNotifier showCursor, + @required TextSelection selection, + @required Color selectionColor, + @required Color cursorColor, + }) : _node = node, + _layerLink = layerLink, + _renderContext = renderContext, + _showCursor = showCursor, + _selection = selection, + _selectionColor = selectionColor, + super() { + this.child = child; + _cursorPainter = CursorPainter(cursorColor); + } + + CursorPainter _cursorPainter; + + set cursorColor(Color value) { + if (_cursorPainter.color != value) { + _cursorPainter.color = value; + markNeedsPaint(); + } + } + + bool _isDirty = true; + + ContainerNode get node => _node; + ContainerNode _node; + void set node(ContainerNode value) { + _node = value; + } + + LayerLink get layerLink => _layerLink; + LayerLink _layerLink; + void set layerLink(LayerLink value) { + if (_layerLink == value) return; + _layerLink = value; + } + + ZefyrRenderContext _renderContext; + void set renderContext(ZefyrRenderContext value) { + if (_renderContext == value) return; + if (attached) _renderContext.removeBox(this); + _renderContext = value; + if (attached) _renderContext.addBox(this); + } + + ValueNotifier _showCursor; + set showCursor(ValueNotifier value) { + assert(value != null); + if (_showCursor == value) return; + if (attached) _showCursor.removeListener(markNeedsCursorPaint); + _showCursor = value; + if (attached) _showCursor.addListener(markNeedsCursorPaint); + markNeedsPaint(); + } + + /// Current document selection. + TextSelection get selection => _selection; + TextSelection _selection; + set selection(TextSelection value) { + if (_selection == value) return; + // TODO: check if selection affects this block (also check previous value) + _selection = value; + markNeedsPaint(); + } + + /// Color of selection. + Color get selectionColor => _selectionColor; + Color _selectionColor; + set selectionColor(Color value) { + if (_selectionColor == value) return; + _selectionColor = value; + markNeedsPaint(); + } + + /// Returns `true` if current selection is collapsed, located within + /// this paragraph and is visible according to tick timer. + bool get isCaretVisible { + return _showCursor.value && containsCaret; + } + + /// Returns `true` if current selection is collapsed and located + /// within this paragraph. + bool get containsCaret { + if (!_selection.isCollapsed) return false; + + final int start = node.documentOffset; + final int end = start + node.length; + final int caretOffset = _selection.extentOffset; + return caretOffset >= start && caretOffset < end; + } + + /// Returns `true` if selection is not collapsed and intersects with this + /// paragraph. + bool get isSelectionVisible { + if (_selection.isCollapsed) return false; + return intersectsWithSelection(_selection); + } + + void markNeedsCursorPaint() { + if (containsCaret) { + markNeedsPaint(); + } + } + + // + // Overridden members of RenderBox + // + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + _showCursor.addListener(markNeedsCursorPaint); + _renderContext.addBox(this); + _renderContext.markDirty(this, _isDirty); + } + + @override + void detach() { + _showCursor.removeListener(markNeedsCursorPaint); + _renderContext.removeBox(this); + super.detach(); + } + + @override + @mustCallSuper + void performLayout() { + super.performLayout(); + _cursorPainter.layout(preferredLineHeight); + // Indicate to render context that this object can be used by other + // layers (selection overlay, for instance). + _isDirty = false; + _renderContext.markDirty(this, false); + } + + @override + void markNeedsLayout() { + // Temporarily remove this object from the render context. + _isDirty = true; + _renderContext.markDirty(this, true); + super.markNeedsLayout(); + } + + @override + void paint(PaintingContext context, Offset offset) { + if (selectionOrder == SelectionOrder.background && isSelectionVisible) { + paintSelection(context, offset, selection, selectionColor); + } + super.paint(context, offset); + if (selectionOrder == SelectionOrder.foreground && isSelectionVisible) { + paintSelection(context, offset, selection, selectionColor); + } + if (isCaretVisible) { + _paintCursor(context, offset); + } + } + + void _paintCursor(PaintingContext context, Offset offset) { + Offset caretOffset = + getOffsetForCaret(_selection.extent, _cursorPainter.prototype); + _cursorPainter.paint(context.canvas, caretOffset + offset); + } + + @override + bool hitTestSelf(Offset position) => true; + + @override + bool hitTest(HitTestResult result, {Offset position}) { + if (size.contains(position)) { + result.add(new BoxHitTestEntry(this, position)); + return true; + } + return false; + } + + // + // Proxy methods + // + + @override + double get preferredLineHeight => child.preferredLineHeight; + + @override + SelectionOrder get selectionOrder => child.selectionOrder; + + @override + void paintSelection(PaintingContext context, Offset offset, + TextSelection selection, Color selectionColor) => + child.paintSelection(context, offset, selection, selectionColor); + + @override + Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) => + child.getOffsetForCaret(position, caretPrototype); + + @override + TextSelection getLocalSelection(TextSelection documentSelection) => + child.getLocalSelection(documentSelection); + + bool intersectsWithSelection(TextSelection selection) => + child.intersectsWithSelection(selection); + + @override + List getEndpointsForSelection(TextSelection selection) => + child.getEndpointsForSelection(selection); + + @override + ui.TextPosition getPositionForOffset(ui.Offset offset) => + child.getPositionForOffset(offset); + + @override + TextRange getWordBoundary(ui.TextPosition position) => + child.getWordBoundary(position); +} + +enum SelectionOrder { + /// Background selection is painted before primary content of editable box. + background, + + /// Foreground selection is painted after primary content of editable box. + foreground, +} + +abstract class RenderEditableBox extends RenderBox { + Node get node; + double get preferredLineHeight; + + TextPosition getPositionForOffset(Offset offset); + List getEndpointsForSelection(TextSelection selection); + + /// Returns the text range of the word at the given offset. Characters not + /// part of a word, such as spaces, symbols, and punctuation, have word breaks + /// on both sides. In such cases, this method will return a text range that + /// contains the given text position. + /// + /// Word boundaries are defined more precisely in Unicode Standard Annex #29 + /// . + /// + /// Valid only after [layout]. + TextRange getWordBoundary(TextPosition position); + + /// Paint order of selection in this editable box. + SelectionOrder get selectionOrder; + + void paintSelection(PaintingContext context, Offset offset, + TextSelection selection, Color selectionColor); + + Offset getOffsetForCaret(TextPosition position, Rect caretPrototype); + + /// Returns part of [documentSelection] local to this box. May return + /// `null`. + /// + /// [documentSelection] must not be collapsed. + TextSelection getLocalSelection(TextSelection documentSelection) { + if (!intersectsWithSelection(documentSelection)) return null; + + int nodeBase = node.documentOffset; + int nodeExtent = nodeBase + node.length; + int base = math.max(0, documentSelection.baseOffset - nodeBase); + int extent = + math.min(documentSelection.extentOffset, nodeExtent) - nodeBase; + return documentSelection.copyWith(baseOffset: base, extentOffset: extent); + } + + /// Returns `true` if this box intersects with document [selection]. + bool intersectsWithSelection(TextSelection selection) { + final int base = node.documentOffset; + final int extent = base + node.length; + return base <= selection.extentOffset && selection.baseOffset <= extent; + } +} diff --git a/zefyr/lib/src/widgets/editable_text.dart b/zefyr/lib/src/widgets/editable_text.dart new file mode 100644 index 00000000..37fdcf41 --- /dev/null +++ b/zefyr/lib/src/widgets/editable_text.dart @@ -0,0 +1,283 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'package:flutter/cupertino.dart'; +import 'package:flutter/widgets.dart'; +import 'package:notus/notus.dart'; + +import 'code.dart'; +import 'common.dart'; +import 'controller.dart'; +import 'cursor_timer.dart'; +import 'editor.dart'; +import 'image.dart'; +import 'input.dart'; +import 'list.dart'; +import 'paragraph.dart'; +import 'quote.dart'; +import 'render_context.dart'; +import 'scope.dart'; +import 'selection.dart'; +import 'theme.dart'; + +/// Core widget responsible for editing Zefyr documents. +/// +/// Depends on presence of [ZefyrTheme] and [ZefyrScope] somewhere up the +/// widget tree. +/// +/// Consider using [ZefyrEditor] which wraps this widget and adds a toolbar to +/// edit style attributes. +class ZefyrEditableText extends StatefulWidget { + const ZefyrEditableText({ + Key key, + @required this.controller, + @required this.focusNode, + @required this.imageDelegate, + this.autofocus: true, + this.enabled: true, + this.padding: const EdgeInsets.symmetric(horizontal: 16.0), + this.physics, + }) : super(key: key); + + final ZefyrController controller; + final FocusNode focusNode; + final ZefyrImageDelegate imageDelegate; + final bool autofocus; + final bool enabled; + final ScrollPhysics physics; + + /// Padding around editable area. + final EdgeInsets padding; + + + + @override + _ZefyrEditableTextState createState() => new _ZefyrEditableTextState(); +} + +class _ZefyrEditableTextState extends State + with AutomaticKeepAliveClientMixin { + // + // New public members + // + + /// Focus node of this widget. + FocusNode get focusNode => widget.focusNode; + + /// Document controlled by this widget. + NotusDocument get document => widget.controller.document; + + /// Current text selection. + TextSelection get selection => widget.controller.selection; + + /// Express interest in interacting with the keyboard. + /// + /// If this control is already attached to the keyboard, this function will + /// request that the keyboard become visible. Otherwise, this function will + /// ask the focus system that it become focused. If successful in acquiring + /// focus, the control will then attach to the keyboard and request that the + /// keyboard become visible. + void requestKeyboard() { + if (focusNode.hasFocus) + _input.openConnection(widget.controller.plainTextEditingValue); + else + FocusScope.of(context).requestFocus(focusNode); + } + + void focusOrUnfocusIfNeeded() { + if (!_didAutoFocus && widget.autofocus && widget.enabled) { + FocusScope.of(context).autofocus(focusNode); + _didAutoFocus = true; + } + if (!widget.enabled && focusNode.hasFocus) { + _didAutoFocus = false; + focusNode.unfocus(); + } + } + + // + // Overridden members of State + // + + + + @override + Widget build(BuildContext context) { +// var reparentIfNeeded = FocusScope.of(context).reparentIfNeeded(focusNode); + + _nodeAttachment.reparent(); + super.build(context); // See AutomaticKeepAliveState. + + Widget body = ListBody(children: _buildChildren(context)); + if (widget.padding != null) { + body = new Padding(padding: widget.padding, child: body); + } + final scrollable = SingleChildScrollView( + physics: widget.physics, + controller: _scrollController, + child: body, + ); + + final overlay = Overlay.of(context, debugRequiredFor: widget); + final layers = [scrollable]; + if (widget.enabled) { + layers.add(ZefyrSelectionOverlay( + controller: widget.controller, + controls: cupertinoTextSelectionControls, + overlay: overlay, + )); + } + + return Stack(fit: StackFit.expand, children: layers); + } + + FocusAttachment _nodeAttachment; + @override + void initState() { + super.initState(); +// FocusScopeNode _node = focusNode; + _nodeAttachment = focusNode.attach(context); + _input = new InputConnectionController(_handleRemoteValueChange); + _updateSubscriptions(); + } + + @override + void didUpdateWidget(ZefyrEditableText oldWidget) { + super.didUpdateWidget(oldWidget); + _updateSubscriptions(oldWidget); + focusOrUnfocusIfNeeded(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final scope = ZefyrScope.of(context); + if (_renderContext != scope.renderContext) { + _renderContext?.removeListener(_handleRenderContextChange); + _renderContext = scope.renderContext; + _renderContext.addListener(_handleRenderContextChange); + } + if (_cursorTimer != scope.cursorTimer) { + _cursorTimer?.stop(); + _cursorTimer = scope.cursorTimer; + _cursorTimer.startOrStop(focusNode, selection); + } + focusOrUnfocusIfNeeded(); + } + + @override + void dispose() { + _cancelSubscriptions(); + super.dispose(); + } + + // + // Overridden members of AutomaticKeepAliveClientMixin + // + + @override + bool get wantKeepAlive => focusNode.hasFocus; + + // + // Private members + // + + final ScrollController _scrollController = ScrollController(); + ZefyrRenderContext _renderContext; + CursorTimer _cursorTimer; + InputConnectionController _input; + bool _didAutoFocus = false; + + List _buildChildren(BuildContext context) { + final result = []; + for (var node in document.root.children) { + result.add(_defaultChildBuilder(context, node)); + } + return result; + } + + Widget _defaultChildBuilder(BuildContext context, Node node) { + if (node is LineNode) { + if (node.hasEmbed) { + return new RawZefyrLine(node: node); + } else if (node.style.contains(NotusAttribute.heading)) { + return new ZefyrHeading(node: node); + } + return new ZefyrParagraph(node: node); + } + + final BlockNode block = node; + final blockStyle = block.style.get(NotusAttribute.block); + if (blockStyle == NotusAttribute.block.code) { + return new ZefyrCode(node: block); + } else if (blockStyle == NotusAttribute.block.bulletList) { + return new ZefyrList(node: block); + } else if (blockStyle == NotusAttribute.block.numberList) { + return new ZefyrList(node: block); + } else if (blockStyle == NotusAttribute.block.quote) { + return new ZefyrQuote(node: block); + } + + throw new UnimplementedError('Block format $blockStyle.'); + } + + void _updateSubscriptions([ZefyrEditableText oldWidget]) { + if (oldWidget == null) { + widget.controller.addListener(_handleLocalValueChange); + focusNode.addListener(_handleFocusChange); + return; + } + + if (widget.controller != oldWidget.controller) { + oldWidget.controller.removeListener(_handleLocalValueChange); + widget.controller.addListener(_handleLocalValueChange); + _input.updateRemoteValue(widget.controller.plainTextEditingValue); + } + if (widget.focusNode != oldWidget.focusNode) { + oldWidget.focusNode.removeListener(_handleFocusChange); + widget.focusNode.addListener(_handleFocusChange); + updateKeepAlive(); + } + } + + void _cancelSubscriptions() { + _renderContext.removeListener(_handleRenderContextChange); + widget.controller.removeListener(_handleLocalValueChange); + focusNode.removeListener(_handleFocusChange); + _input.closeConnection(); + _cursorTimer.stop(); + } + + // Triggered for both text and selection changes. + void _handleLocalValueChange() { + if (widget.enabled && + widget.controller.lastChangeSource == ChangeSource.local) { + // Only request keyboard for user actions. + requestKeyboard(); + } + _input.updateRemoteValue(widget.controller.plainTextEditingValue); + _cursorTimer.startOrStop(focusNode, selection); + setState(() { + // nothing to update internally. + }); + } + + void _handleFocusChange() { + _input.openOrCloseConnection( + focusNode, widget.controller.plainTextEditingValue); + _cursorTimer.startOrStop(focusNode, selection); + updateKeepAlive(); + } + + void _handleRemoteValueChange( + int start, String deleted, String inserted, TextSelection selection) { + widget.controller + .replaceText(start, deleted.length, inserted, selection: selection); + } + + void _handleRenderContextChange() { + setState(() { + // nothing to update internally. + }); + } +} diff --git a/zefyr/lib/src/widgets/editor.dart b/zefyr/lib/src/widgets/editor.dart new file mode 100644 index 00000000..4a37337e --- /dev/null +++ b/zefyr/lib/src/widgets/editor.dart @@ -0,0 +1,164 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'package:flutter/widgets.dart'; + +import 'controller.dart'; +import 'editable_text.dart'; +import 'image.dart'; +import 'scaffold.dart'; +import 'scope.dart'; +import 'theme.dart'; +import 'toolbar.dart'; + +/// Widget for editing Zefyr documents. +class ZefyrEditor extends StatefulWidget { + const ZefyrEditor({ + Key key, + @required this.controller, + @required this.focusNode, + this.autofocus: true, + this.enabled: true, + this.padding: const EdgeInsets.symmetric(horizontal: 16.0), + this.toolbarDelegate, + this.imageDelegate, + this.physics, + }) : super(key: key); + + final ZefyrController controller; + final FocusNode focusNode; + final bool autofocus; + final bool enabled; + final ZefyrToolbarDelegate toolbarDelegate; + final ZefyrImageDelegate imageDelegate; + final ScrollPhysics physics; + + /// Padding around editable area. + final EdgeInsets padding; + + @override + _ZefyrEditorState createState() => new _ZefyrEditorState(); +} + +class _ZefyrEditorState extends State { + ZefyrImageDelegate _imageDelegate; + ZefyrScope _scope; + ZefyrThemeData _themeData; + GlobalKey _toolbarKey; + ZefyrScaffoldState _scaffold; + + bool get hasToolbar => _toolbarKey != null; + + void showToolbar() { + assert(_toolbarKey == null); + _toolbarKey = GlobalKey(); + _scaffold.showToolbar(buildToolbar); + } + + void hideToolbar() { + if (_toolbarKey == null) return; + _scaffold.hideToolbar(); + _toolbarKey = null; + } + + Widget buildToolbar(BuildContext context) { + return ZefyrTheme( + data: _themeData, + child: ZefyrToolbar( + key: _toolbarKey, + editor: _scope, + delegate: widget.toolbarDelegate, + ), + ); + } + + void _handleChange() { + if (_scope.focusOwner == FocusOwner.none) { + hideToolbar(); + } else if (!hasToolbar) { + showToolbar(); + } else { + // TODO: is there a nicer way to do this? + WidgetsBinding.instance.addPostFrameCallback((_) { + _toolbarKey?.currentState?.markNeedsRebuild(); + }); + } + } + + @override + void initState() { + super.initState(); + _imageDelegate = widget.imageDelegate ?? new ZefyrDefaultImageDelegate(); + } + + @override + void didUpdateWidget(ZefyrEditor oldWidget) { + super.didUpdateWidget(oldWidget); + _scope.controller = widget.controller; + _scope.focusNode = widget.focusNode; + if (widget.imageDelegate != oldWidget.imageDelegate) { + _imageDelegate = widget.imageDelegate ?? new ZefyrDefaultImageDelegate(); + _scope.imageDelegate = _imageDelegate; + } + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final parentTheme = ZefyrTheme.of(context, nullOk: true); + final fallbackTheme = ZefyrThemeData.fallback(context); + _themeData = (parentTheme != null) + ? fallbackTheme.merge(parentTheme) + : fallbackTheme; + + if (_scope == null) { + _scope = ZefyrScope.editable( + imageDelegate: _imageDelegate, + controller: widget.controller, + focusNode: widget.focusNode, + focusScope: FocusScope.of(context), + ); + _scope.addListener(_handleChange); + } else { + final focusScope = FocusScope.of(context); + _scope.focusScope = focusScope; + } + + final scaffold = ZefyrScaffold.of(context); + if (_scaffold != scaffold) { + bool didHaveToolbar = hasToolbar; + hideToolbar(); + _scaffold = scaffold; + if (didHaveToolbar) showToolbar(); + } + } + + @override + void dispose() { + hideToolbar(); + _scope.removeListener(_handleChange); + _scope.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Widget editable = new ZefyrEditableText( + controller: _scope.controller, + focusNode: _scope.focusNode, + imageDelegate: _scope.imageDelegate, + autofocus: widget.autofocus, + enabled: widget.enabled, + padding: widget.padding, + physics: widget.physics, + ); + + return ZefyrTheme( + data: _themeData, + child: ZefyrScopeAccess( + scope: _scope, + child: editable, + ), + ); + } +} diff --git a/zefyr/lib/src/widgets/field.dart b/zefyr/lib/src/widgets/field.dart new file mode 100644 index 00000000..d23f3ca7 --- /dev/null +++ b/zefyr/lib/src/widgets/field.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; + +import 'controller.dart'; +import 'editor.dart'; +import 'image.dart'; +import 'toolbar.dart'; + +/// Zefyr editor with material design decorations. +class ZefyrField extends StatefulWidget { + /// Decoration to paint around this editor. + final InputDecoration decoration; + + /// Height of this editor field. + final double height; + final ZefyrController controller; + final FocusNode focusNode; + final bool autofocus; + final bool enabled; + final ZefyrToolbarDelegate toolbarDelegate; + final ZefyrImageDelegate imageDelegate; + final ScrollPhysics physics; + + const ZefyrField({ + Key key, + this.decoration, + this.height, + this.controller, + this.focusNode, + this.autofocus: false, + this.enabled, + this.toolbarDelegate, + this.imageDelegate, + this.physics, + }) : super(key: key); + + @override + _ZefyrFieldState createState() => _ZefyrFieldState(); +} + +class _ZefyrFieldState extends State { + @override + Widget build(BuildContext context) { + Widget child = ZefyrEditor( + padding: EdgeInsets.symmetric(vertical: 6.0), + controller: widget.controller, + focusNode: widget.focusNode, + autofocus: widget.autofocus, + enabled: widget.enabled ?? true, + toolbarDelegate: widget.toolbarDelegate, + imageDelegate: widget.imageDelegate, + physics: widget.physics, + ); + + if (widget.height != null) { + child = ConstrainedBox( + constraints: BoxConstraints.tightFor(height: widget.height), + child: child, + ); + } + + return AnimatedBuilder( + animation: + Listenable.merge([widget.focusNode, widget.controller]), + builder: (BuildContext context, Widget child) { + return InputDecorator( + decoration: _getEffectiveDecoration(), + isFocused: widget.focusNode.hasFocus, + isEmpty: widget.controller.document.length == 1, + child: child, + ); + }, + child: child, + ); + } + + InputDecoration _getEffectiveDecoration() { + final InputDecoration effectiveDecoration = + (widget.decoration ?? const InputDecoration()) + .applyDefaults(Theme.of(context).inputDecorationTheme) + .copyWith( + enabled: widget.enabled ?? true, + ); + + return effectiveDecoration; + } +} diff --git a/zefyr/lib/src/widgets/horizontal_rule.dart b/zefyr/lib/src/widgets/horizontal_rule.dart new file mode 100644 index 00000000..d9011f0e --- /dev/null +++ b/zefyr/lib/src/widgets/horizontal_rule.dart @@ -0,0 +1,121 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:notus/notus.dart'; + +import 'editable_box.dart'; + +class ZefyrHorizontalRule extends LeafRenderObjectWidget { + ZefyrHorizontalRule({@required this.node}) : assert(node != null); + + final EmbedNode node; + + @override + RenderHorizontalRule createRenderObject(BuildContext context) { + return new RenderHorizontalRule(node: node); + } + + @override + void updateRenderObject( + BuildContext context, RenderHorizontalRule renderObject) { + renderObject..node = node; + } +} + +class RenderHorizontalRule extends RenderEditableBox { + static const _kPaddingBottom = 24.0; + static const _kThickness = 3.0; + static const _kHeight = _kThickness + _kPaddingBottom; + + RenderHorizontalRule({ + @required EmbedNode node, + }) : _node = node; + + @override + EmbedNode get node => _node; + EmbedNode _node; + set node(EmbedNode value) { + if (_node == value) return; + _node = value; + markNeedsPaint(); + } + + @override + double get preferredLineHeight => size.height; + + @override + SelectionOrder get selectionOrder => SelectionOrder.background; + + @override + List getEndpointsForSelection(TextSelection selection) { + TextSelection local = getLocalSelection(selection); + if (local.isCollapsed) { + final dx = local.extentOffset == 0 ? 0.0 : size.width; + return [ + new ui.TextBox.fromLTRBD(dx, 0.0, dx, size.height, TextDirection.ltr), + ]; + } + + return [ + new ui.TextBox.fromLTRBD(0.0, 0.0, 0.0, size.height, TextDirection.ltr), + new ui.TextBox.fromLTRBD( + size.width, 0.0, size.width, size.height, TextDirection.ltr), + ]; + } + + @override + void performLayout() { + assert(constraints.hasBoundedWidth); + size = new Size(constraints.maxWidth, _kHeight); + } + + @override + void paint(PaintingContext context, Offset offset) { + final rect = new Rect.fromLTWH(0.0, 0.0, size.width, _kThickness); + final paint = new ui.Paint()..color = Colors.grey.shade200; + context.canvas.drawRect(rect.shift(offset), paint); + } + + @override + TextPosition getPositionForOffset(Offset offset) { + int position = _node.documentOffset; + + if (offset.dx > size.width / 2) { + position++; + } + return new TextPosition(offset: position); + } + + @override + TextRange getWordBoundary(TextPosition position) { + final start = _node.documentOffset; + return new TextRange(start: start, end: start + 1); + } + + @override + void paintSelection(PaintingContext context, Offset offset, + TextSelection selection, Color selectionColor) { + final localSelection = getLocalSelection(selection); + assert(localSelection != null); + if (!localSelection.isCollapsed) { + final Paint paint = new Paint()..color = selectionColor; + final rect = new Rect.fromLTWH(0.0, 0.0, size.width, _kHeight); + context.canvas.drawRect(rect.shift(offset), paint); + } + } + + @override + Offset getOffsetForCaret(ui.TextPosition position, ui.Rect caretPrototype) { + final pos = position.offset - node.documentOffset; + Offset caretOffset = Offset.zero; + if (pos == 1) { + caretOffset = caretOffset + new Offset(size.width - 1.0, 0.0); + } + return caretOffset; + } +} diff --git a/zefyr/lib/src/widgets/image.dart b/zefyr/lib/src/widgets/image.dart new file mode 100644 index 00000000..be096d06 --- /dev/null +++ b/zefyr/lib/src/widgets/image.dart @@ -0,0 +1,236 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'dart:async'; +import 'dart:io'; +import 'dart:math' as math; +import 'dart:ui' as ui; + +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:notus/notus.dart'; +import 'package:image_picker/image_picker.dart'; + +import 'editable_box.dart'; + +abstract class ZefyrImageDelegate { + /// Builds image widget for specified [imageSource] and [context]. + Widget buildImage(BuildContext context, String imageSource); + + /// Picks an image from specified [source]. + /// + /// Returns unique string key for the selected image. Returned key is stored + /// in the document. + Future pickImage(S source); +} + +class ZefyrDefaultImageDelegate implements ZefyrImageDelegate { + @override + Widget buildImage(BuildContext context, String imageSource) { + final file = new File.fromUri(Uri.parse(imageSource)); + final image = new FileImage(file); + return new Image(image: image); + } + + @override + Future pickImage(ImageSource source) async { + final file = await ImagePicker.pickImage(source: source); + if (file == null) return null; + return file.uri.toString(); + } +} + +class ZefyrImage extends StatefulWidget { + const ZefyrImage({Key key, @required this.node, @required this.delegate}) + : super(key: key); + + final EmbedNode node; + final ZefyrImageDelegate delegate; + + @override + _ZefyrImageState createState() => _ZefyrImageState(); +} + +class _ZefyrImageState extends State { + String get imageSource { + EmbedAttribute attribute = widget.node.style.get(NotusAttribute.embed); + return attribute.value['source'] as String; + } + + @override + Widget build(BuildContext context) { + final image = widget.delegate.buildImage(context, imageSource); + return _EditableImage( + child: image, + node: widget.node, + ); + } +} + +class _EditableImage extends SingleChildRenderObjectWidget { + _EditableImage({@required Widget child, @required this.node}) + : assert(node != null), + super(child: child); + + final EmbedNode node; + + @override + RenderEditableImage createRenderObject(BuildContext context) { + return new RenderEditableImage(node: node); + } + + @override + void updateRenderObject( + BuildContext context, RenderEditableImage renderObject) { + renderObject..node = node; + } +} + +class RenderEditableImage extends RenderBox + with RenderObjectWithChildMixin, RenderProxyBoxMixin + implements RenderEditableBox { + static const kPaddingBottom = 24.0; + + RenderEditableImage({ + RenderImage child, + @required EmbedNode node, + }) : _node = node { + this.child = child; + } + + @override + EmbedNode get node => _node; + EmbedNode _node; + void set node(EmbedNode value) { + _node = value; + } + + // TODO: Customize caret height offset instead of adjusting here by 2px. + @override + double get preferredLineHeight => size.height - kPaddingBottom + 2.0; + + @override + SelectionOrder get selectionOrder => SelectionOrder.foreground; + + @override + TextSelection getLocalSelection(TextSelection documentSelection) { + if (!intersectsWithSelection(documentSelection)) return null; + + int nodeBase = node.documentOffset; + int nodeExtent = nodeBase + node.length; + int base = math.max(0, documentSelection.baseOffset - nodeBase); + int extent = + math.min(documentSelection.extentOffset, nodeExtent) - nodeBase; + return documentSelection.copyWith(baseOffset: base, extentOffset: extent); + } + + @override + List getEndpointsForSelection(TextSelection selection) { + TextSelection local = getLocalSelection(selection); + if (local.isCollapsed) { + final dx = local.extentOffset == 0 ? _childOffset.dx : size.width; + return [ + new ui.TextBox.fromLTRBD( + dx, 0.0, dx, size.height - kPaddingBottom, TextDirection.ltr), + ]; + } + + final rect = _childRect; + return [ + new ui.TextBox.fromLTRBD( + rect.left, rect.top, rect.left, rect.bottom, TextDirection.ltr), + new ui.TextBox.fromLTRBD( + rect.right, rect.top, rect.right, rect.bottom, TextDirection.ltr), + ]; + } + + @override + TextPosition getPositionForOffset(Offset offset) { + int position = _node.documentOffset; + + if (offset.dx > size.width / 2) { + position++; + } + return new TextPosition(offset: position); + } + + @override + TextRange getWordBoundary(TextPosition position) { + final start = _node.documentOffset; + return new TextRange(start: start, end: start + 1); + } + + @override + bool intersectsWithSelection(TextSelection selection) { + final int base = node.documentOffset; + final int extent = base + node.length; + return base <= selection.extentOffset && selection.baseOffset <= extent; + } + + @override + Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) { + final pos = position.offset - node.documentOffset; + Offset caretOffset = _childOffset - new Offset(kHorizontalPadding, 0.0); + if (pos == 1) { + caretOffset = caretOffset + + new Offset(_lastChildSize.width + kHorizontalPadding, 0.0); + } + return caretOffset; + } + + @override + void paintSelection(PaintingContext context, Offset offset, + TextSelection selection, Color selectionColor) { + final localSelection = getLocalSelection(selection); + assert(localSelection != null); + if (!localSelection.isCollapsed) { + final Paint paint = new Paint() + ..color = selectionColor + ..style = PaintingStyle.stroke + ..strokeWidth = 3.0; + final rect = new Rect.fromLTWH( + 0.0, 0.0, _lastChildSize.width, _lastChildSize.height); + context.canvas.drawRect(rect.shift(offset + _childOffset), paint); + } + } + + void paint(PaintingContext context, Offset offset) { + super.paint(context, offset + _childOffset); + } + + static const double kHorizontalPadding = 1.0; + + Size _lastChildSize; + + Offset get _childOffset { + final dx = (size.width - _lastChildSize.width) / 2 + kHorizontalPadding; + final dy = (size.height - _lastChildSize.height - kPaddingBottom) / 2; + return new Offset(dx, dy); + } + + Rect get _childRect { + return new Rect.fromLTWH(_childOffset.dx, _childOffset.dy, + _lastChildSize.width, _lastChildSize.height); + } + + @override + void performLayout() { + assert(constraints.hasBoundedWidth); + if (child != null) { + // Make constraints use 16:9 aspect ratio. + final width = constraints.maxWidth - kHorizontalPadding * 2; + final childConstraints = constraints.copyWith( + minWidth: 0.0, + maxWidth: width, + minHeight: 0.0, + maxHeight: (width * 9 / 16).floorToDouble(), + ); + child.layout(childConstraints, parentUsesSize: true); + _lastChildSize = child.size; + size = new Size( + constraints.maxWidth, _lastChildSize.height + kPaddingBottom); + } else { + performResize(); + } + } +} diff --git a/zefyr/lib/src/widgets/input.dart b/zefyr/lib/src/widgets/input.dart new file mode 100644 index 00000000..d006d976 --- /dev/null +++ b/zefyr/lib/src/widgets/input.dart @@ -0,0 +1,198 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'package:flutter/services.dart'; +import 'package:flutter/src/foundation/diagnostics.dart'; +import 'package:flutter/widgets.dart'; +import 'package:zefyr/util.dart'; + +typedef RemoteValueChanged = Function( + int start, String deleted, String inserted, TextSelection selection); + +class InputConnectionController implements TextInputClient { + InputConnectionController(this.onValueChanged) + : assert(onValueChanged != null); + + // + // New public members + // + + final RemoteValueChanged onValueChanged; + + /// Returns `true` if there is open input connection. + bool get hasConnection => + _textInputConnection != null && _textInputConnection.attached; + + /// Opens or closes input connection based on the current state of + /// [focusNode] and [value]. + void openOrCloseConnection(FocusNode focusNode, TextEditingValue value) { + if (focusNode.hasFocus && focusNode.consumeKeyboardToken()) { + openConnection(value); + } else if (!focusNode.hasFocus) { + closeConnection(); + } + } + + void openConnection(TextEditingValue value) { + if (!hasConnection) { + _lastKnownRemoteTextEditingValue = value; + _textInputConnection = TextInput.attach( + this, + new TextInputConfiguration( + inputType: TextInputType.multiline, + obscureText: false, + autocorrect: true, + inputAction: TextInputAction.newline, + textCapitalization: TextCapitalization.sentences, + ), + )..setEditingState(value); + _sentRemoteValues.add(value); + } + _textInputConnection.show(); + } + + /// Closes input connection if it's currently open. Otherwise does nothing. + void closeConnection() { + if (hasConnection) { + _textInputConnection.close(); + _textInputConnection = null; + _lastKnownRemoteTextEditingValue = null; + _sentRemoteValues.clear(); + } + } + + /// Updates remote value based on current state of [document] and + /// [selection]. + /// + /// This method may not actually send an update to native side if it thinks + /// remote value is up to date or identical. + void updateRemoteValue(TextEditingValue value) { + if (!hasConnection) return; + + // Since we don't keep track of composing range in value provided by + // ZefyrController we need to add it here manually before comparing + // with the last known remote value. + // It is important to prevent excessive remote updates as it can cause + // race conditions. + final actualValue = value.copyWith( + composing: _lastKnownRemoteTextEditingValue.composing, + ); + + if (actualValue == _lastKnownRemoteTextEditingValue) return; + + bool shouldRemember = value.text != _lastKnownRemoteTextEditingValue.text; + _lastKnownRemoteTextEditingValue = actualValue; + _textInputConnection.setEditingState(actualValue); + if (shouldRemember) { + // Only keep track if text changed (selection changes are not relevant) + _sentRemoteValues.add(actualValue); + } + } + + // + // Overridden members + // + + @override + void performAction(TextInputAction action) { + // no-op + } + + @override + void updateEditingValue(TextEditingValue value) { + if (_sentRemoteValues.contains(value)) { + /// There is a race condition in Flutter text input plugin where sending + /// updates to native side too often results in broken behavior. + /// TextInputConnection.setEditingValue is an async call to native side. + /// For each such call native side _always_ sends update which triggers + /// this method (updateEditingValue) with the same value we've sent it. + /// If multiple calls to setEditingValue happen too fast and we only + /// track the last sent value then there is no way for us to filter out + /// automatic callbacks from native side. + /// Therefore we have to keep track of all values we send to the native + /// side and when we see this same value appear here we skip it. + /// This is fragile but it's probably the only available option. + _sentRemoteValues.remove(value); + return; + } + + if (_lastKnownRemoteTextEditingValue == value) { + // There is no difference between this value and the last known value. + return; + } + + // Check if only composing range changed. + if (_lastKnownRemoteTextEditingValue.text == value.text && + _lastKnownRemoteTextEditingValue.selection == value.selection) { + // This update only modifies composing range. Since we don't keep track + // of composing range in Zefyr we just need to update last known value + // here. + // Note: this check fixes an issue on Android when it sends + // composing updates separately from regular changes for text and + // selection. + _lastKnownRemoteTextEditingValue = value; + return; + } + + // Note Flutter (unintentionally?) silences errors occurred during + // text input update, so we have to report it ourselves. + // For more details see https://github.com/flutter/flutter/issues/19191 + // TODO: remove try-catch when/if Flutter stops silencing these errors. + try { + final effectiveLastKnownValue = _lastKnownRemoteTextEditingValue; + _lastKnownRemoteTextEditingValue = value; + final oldText = effectiveLastKnownValue.text; + final text = value.text; + final cursorPosition = value.selection.extentOffset; + final diff = fastDiff(oldText, text, cursorPosition); + onValueChanged(diff.start, diff.deleted, diff.inserted, value.selection); + } catch (e, trace) { + FlutterError.reportError(new FlutterErrorDetails( + exception: e, + stack: trace, + library: 'Zefyr', +// context: 'while updating editing value', + context: new TextNode() + )); + rethrow; + } + } + + + // + // Private members + // + + final List _sentRemoteValues = []; + TextInputConnection _textInputConnection; + TextEditingValue _lastKnownRemoteTextEditingValue; + + @override + void updateFloatingCursor(RawFloatingCursorPoint point) { + // TODO: implement updateFloatingCursor + } +} + +class TextNode extends DiagnosticsNode{ + @override + List getChildren() { + // TODO: implement getChildren + return null; + } + + @override + List getProperties() { + // TODO: implement getProperties + return null; + } + + @override + String toDescription({TextTreeConfiguration parentConfiguration}) { + // TODO: implement toDescription + return null; + } + + @override + Object get value => 'while updating editing value'; + +} diff --git a/zefyr/lib/src/widgets/list.dart b/zefyr/lib/src/widgets/list.dart new file mode 100644 index 00000000..a3479e58 --- /dev/null +++ b/zefyr/lib/src/widgets/list.dart @@ -0,0 +1,86 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'package:flutter/material.dart'; +import 'package:notus/notus.dart'; + +import 'common.dart'; +import 'paragraph.dart'; +import 'theme.dart'; + +/// Represents number lists and bullet lists in a Zefyr editor. +class ZefyrList extends StatelessWidget { + const ZefyrList({Key key, @required this.node}) : super(key: key); + + final BlockNode node; + + @override + Widget build(BuildContext context) { + final theme = ZefyrTheme.of(context); + List items = []; + int index = 1; + for (var line in node.children) { + items.add(_buildItem(line, index)); + index++; + } + + final isNumberList = + node.style.get(NotusAttribute.block) == NotusAttribute.block.numberList; + EdgeInsets padding = isNumberList + ? theme.blockTheme.numberList.padding + : theme.blockTheme.bulletList.padding; + padding = padding.copyWith(left: theme.indentSize); + + return new Padding( + padding: padding, + child: new Column(children: items), + ); + } + + Widget _buildItem(Node node, int index) { + LineNode line = node; + return new ZefyrListItem(index: index, node: line); + } +} + +/// An item in a [ZefyrList]. +class ZefyrListItem extends StatelessWidget { + ZefyrListItem({Key key, this.index, this.node}) : super(key: key); + + final int index; + final LineNode node; + + @override + Widget build(BuildContext context) { + final BlockNode block = node.parent; + final style = block.style.get(NotusAttribute.block); + final theme = ZefyrTheme.of(context); + final bulletText = + (style == NotusAttribute.block.bulletList) ? '•' : '$index.'; + + TextStyle textStyle; + Widget content; + EdgeInsets padding; + + if (node.style.contains(NotusAttribute.heading)) { + final headingTheme = ZefyrHeading.themeOf(node, context); + textStyle = headingTheme.textStyle; + padding = headingTheme.padding; + content = new ZefyrHeading(node: node); + } else { + textStyle = theme.paragraphTheme.textStyle; + content = new RawZefyrLine(node: node, style: textStyle); + } + + Widget bullet = + SizedBox(width: 24.0, child: Text(bulletText, style: textStyle)); + if (padding != null) { + bullet = Padding(padding: padding, child: bullet); + } + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [bullet, Expanded(child: content)], + ); + } +} diff --git a/zefyr/lib/src/widgets/paragraph.dart b/zefyr/lib/src/widgets/paragraph.dart new file mode 100644 index 00000000..222b5bae --- /dev/null +++ b/zefyr/lib/src/widgets/paragraph.dart @@ -0,0 +1,68 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'package:flutter/material.dart'; +import 'package:notus/notus.dart'; + +import 'common.dart'; +import 'theme.dart'; + +/// Represents regular paragraph line in a Zefyr editor. +class ZefyrParagraph extends StatelessWidget { + ZefyrParagraph({Key key, @required this.node, this.blockStyle}) + : super(key: key); + + final LineNode node; + final TextStyle blockStyle; + + @override + Widget build(BuildContext context) { + final theme = ZefyrTheme.of(context); + TextStyle style = theme.paragraphTheme.textStyle; + if (blockStyle != null) { + style = style.merge(blockStyle); + } + return new RawZefyrLine( + node: node, + style: style, + padding: theme.paragraphTheme.padding, + ); + } +} + +/// Represents heading-styled line in [ZefyrEditor]. +class ZefyrHeading extends StatelessWidget { + ZefyrHeading({Key key, @required this.node, this.blockStyle}) + : assert(node.style.contains(NotusAttribute.heading)), + super(key: key); + + final LineNode node; + final TextStyle blockStyle; + + @override + Widget build(BuildContext context) { + final theme = themeOf(node, context); + TextStyle style = theme.textStyle; + if (blockStyle != null) { + style = style.merge(blockStyle); + } + return new RawZefyrLine( + node: node, + style: style, + padding: theme.padding, + ); + } + + static StyleTheme themeOf(LineNode node, BuildContext context) { + final theme = ZefyrTheme.of(context); + final style = node.style.get(NotusAttribute.heading); + if (style == NotusAttribute.heading.level1) { + return theme.headingTheme.level1; + } else if (style == NotusAttribute.heading.level2) { + return theme.headingTheme.level2; + } else if (style == NotusAttribute.heading.level3) { + return theme.headingTheme.level3; + } + throw new UnimplementedError('Unsupported heading style $style'); + } +} diff --git a/zefyr/lib/src/widgets/quote.dart b/zefyr/lib/src/widgets/quote.dart new file mode 100644 index 00000000..a9eacd13 --- /dev/null +++ b/zefyr/lib/src/widgets/quote.dart @@ -0,0 +1,55 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'package:flutter/material.dart'; +import 'package:notus/notus.dart'; + +import 'paragraph.dart'; +import 'theme.dart'; + +/// Represents a quote block in a Zefyr editor. +class ZefyrQuote extends StatelessWidget { + const ZefyrQuote({Key key, @required this.node}) : super(key: key); + + final BlockNode node; + + @override + Widget build(BuildContext context) { + final theme = ZefyrTheme.of(context); + final style = theme.blockTheme.quote.textStyle; + List items = []; + for (var line in node.children) { + items.add(_buildLine(line, style, theme.indentSize)); + } + + return Padding( + padding: theme.blockTheme.quote.padding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: items, + ), + ); + } + + Widget _buildLine(Node node, TextStyle blockStyle, double indentSize) { + LineNode line = node; + + Widget content; + if (line.style.contains(NotusAttribute.heading)) { + content = new ZefyrHeading(node: line, blockStyle: blockStyle); + } else { + content = new ZefyrParagraph(node: line, blockStyle: blockStyle); + } + + final row = Row(children: [Expanded(child: content)]); + return Container( + decoration: BoxDecoration( + border: Border( + left: BorderSide(width: 4.0, color: Colors.grey.shade300), + ), + ), + padding: EdgeInsets.only(left: indentSize), + child: row, + ); + } +} diff --git a/zefyr/lib/src/widgets/render_context.dart b/zefyr/lib/src/widgets/render_context.dart new file mode 100644 index 00000000..1e88aa53 --- /dev/null +++ b/zefyr/lib/src/widgets/render_context.dart @@ -0,0 +1,146 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import 'editable_box.dart'; + +/// Registry of all [RenderEditableProxyBox]es inside a [ZefyrEditableText]. +/// +/// Provides access to all currently active [RenderEditableProxyBox] +/// instances of a [ZefyrEditableText]. +/// +/// Use [boxForTextOffset] or [boxForGlobalPoint] to retrieve a +/// specific box. +/// +/// The [addBox], [removeBox] and [markDirty] are intended to be +/// only used by [RenderEditableProxyBox] objects to register with a rendering +/// context. +/// +/// ### Life cycle details +/// +/// When a box object is attached to rendering pipeline it registers +/// itself with a render scope by calling [addBox]. At this point the context +/// treats this object as "dirty" and query methods like [boxForTextOffset] +/// still exclude this object from returned results. +/// +/// When this box considers itself initialized it calls [markDirty] with +/// `isDirty` set to `false` which activates it. At this point query methods +/// include this object in results. +/// +/// When a box is rebuilt it may deactivate itself by calling [markDirty] +/// again. +/// +/// When a box is detached from rendering pipeline it unregisters +/// itself by calling [removeBox]. +class ZefyrRenderContext extends ChangeNotifier { + final Set _dirtyBoxes = new Set(); + final Set _activeBoxes = new Set(); + + Set get dirty => _dirtyBoxes; + Set get active => _activeBoxes; + + bool _disposed = false; + + /// Adds [box] to this context. The box is considered "dirty" at + /// this point and is not included in query results of `boxFor*` + /// methods. + void addBox(RenderEditableProxyBox box) { + assert(!_disposed); + _dirtyBoxes.add(box); + } + + /// Removes [box] from this render context. + void removeBox(RenderEditableProxyBox box) { + assert(!_disposed); + _dirtyBoxes.remove(box); + _activeBoxes.remove(box); + notifyListeners(); + } + + void markDirty(RenderEditableProxyBox box, bool isDirty) { + assert(!_disposed); + + var collection = isDirty ? _dirtyBoxes : _activeBoxes; + if (collection.contains(box)) return; + + if (isDirty) { + _activeBoxes.remove(box); + _dirtyBoxes.add(box); + } else { + _dirtyBoxes.remove(box); + _activeBoxes.add(box); + } + notifyListeners(); + } + + /// Returns box containing character at specified document [offset]. + RenderEditableProxyBox boxForTextOffset(int offset) { + assert(!_disposed); + return _activeBoxes.firstWhere( + (p) => p.node.containsOffset(offset), + orElse: _null, + ); + } + + /// Returns box located at specified global [point] on the screen or + /// `null`. + RenderEditableProxyBox boxForGlobalPoint(Offset point) { + assert(!_disposed); + return _activeBoxes.firstWhere((p) { + final localPoint = p.globalToLocal(point); + return p.size.contains(localPoint); + }, orElse: _null); + } + + /// Returns closest render box to the specified global [point]. + /// + /// If [point] is inside of one of active render boxes that box is returned. + /// If no box found this method checks if [point] is to the left or to the right + /// side of a box, e.g. if vertical offset of this point is inside of one of + /// the active boxes. If it is then that box is returned. If not then this + /// method picks a box with shortest vertical distance to this [point]. + RenderEditableProxyBox closestBoxForGlobalPoint(Offset point) { + assert(!_disposed); + if (_activeBoxes.isEmpty) return null; + RenderEditableProxyBox box = boxForGlobalPoint(point); + if (box != null) return box; + + box = _activeBoxes.firstWhere((p) { + final localPoint = p.globalToLocal(point); + return (localPoint.dy >= 0 && localPoint.dy < p.size.height); + }, orElse: _null); + if (box != null) return box; + + box = _activeBoxes.map((p) { + final localPoint = p.globalToLocal(point); + final distance = localPoint.dy - p.size.height; + return new MapEntry(distance.abs(), p); + }).reduce((a, b) { + return (a.key <= b.key) ? a : b; + }).value; + + return box; + } + + static Null _null() => null; + + @override + void dispose() { + _disposed = true; + _activeBoxes.clear(); + _dirtyBoxes.clear(); + super.dispose(); + } + + @override + void notifyListeners() { + /// Ensures listeners are not notified during rendering phase where they + /// cannot react by updating their state or rebuilding. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_disposed) return; + super.notifyListeners(); + }); + } +} diff --git a/zefyr/lib/src/widgets/rich_text.dart b/zefyr/lib/src/widgets/rich_text.dart new file mode 100644 index 00000000..f2eff9c9 --- /dev/null +++ b/zefyr/lib/src/widgets/rich_text.dart @@ -0,0 +1,223 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'dart:math' as math; +import 'dart:ui' as ui; + +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:notus/notus.dart'; + +import 'caret.dart'; +import 'editable_box.dart'; + +/// Represents single paragraph of Zefyr rich-text. +class ZefyrRichText extends LeafRenderObjectWidget { + ZefyrRichText({ + @required this.node, + @required this.text, + }) : assert(node != null && text != null); + + final LineNode node; + final TextSpan text; + + @override + RenderObject createRenderObject(BuildContext context) { + return new RenderZefyrParagraph( + text, + node: node, + textDirection: Directionality.of(context), + ); + } + + @override + void updateRenderObject( + BuildContext context, RenderZefyrParagraph renderObject) { + renderObject + ..text = text + ..node = node; + } +} + +class RenderZefyrParagraph extends RenderParagraph + implements RenderEditableBox { + RenderZefyrParagraph( + TextSpan text, { + @required LineNode node, + TextAlign textAlign: TextAlign.start, + @required TextDirection textDirection, + bool softWrap: true, + TextOverflow overflow: TextOverflow.clip, + double textScaleFactor: 1.0, + int maxLines, + }) : _node = node, + _prototypePainter = new TextPainter( + text: new TextSpan(text: '.', style: text.style), + textAlign: textAlign, + textDirection: textDirection, + textScaleFactor: textScaleFactor, + ), + super( + text, + textAlign: textAlign, + textDirection: textDirection, + softWrap: softWrap, + overflow: overflow, + textScaleFactor: textScaleFactor, + maxLines: maxLines, + ); + + LineNode get node => _node; + LineNode _node; + void set node(LineNode value) { + _node = value; + } + + @override + double get preferredLineHeight => _prototypePainter.height; + + @override + SelectionOrder get selectionOrder => SelectionOrder.background; + + @override + TextSelection getLocalSelection(TextSelection documentSelection) { + if (!intersectsWithSelection(documentSelection)) return null; + + int nodeBase = node.documentOffset; + int nodeExtent = nodeBase + node.length; + int base = math.max(0, documentSelection.baseOffset - nodeBase); + int extent = + math.min(documentSelection.extentOffset, nodeExtent) - nodeBase; + return documentSelection.copyWith(baseOffset: base, extentOffset: extent); + } + + @override + TextPosition getPositionForOffset(Offset offset) { + final position = super.getPositionForOffset(offset); + return new TextPosition( + offset: _node.documentOffset + position.offset, + affinity: position.affinity, + ); + } + + @override + TextRange getWordBoundary(TextPosition position) { + final localPosition = new TextPosition( + offset: position.offset - _node.documentOffset, + affinity: position.affinity, + ); + final localRange = super.getWordBoundary(localPosition); + return new TextRange( + start: _node.documentOffset + localRange.start, + end: _node.documentOffset + localRange.end, + ); + } + + @override + Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) { + final localPosition = new TextPosition( + offset: position.offset - _node.documentOffset, + affinity: position.affinity, + ); + return super.getOffsetForCaret(localPosition, caretPrototype); + } + + // This method works around some issues in getBoxesForSelection and handles + // edge-case with our TextSpan objects not having last line-break character. + @override + List getEndpointsForSelection(TextSelection selection) { + TextSelection local = getLocalSelection(selection); + if (local.isCollapsed) { + final caret = CursorPainter.buildPrototype(preferredLineHeight); + final offset = getOffsetForCaret(local.extent, caret); + return [ + new ui.TextBox.fromLTRBD( + offset.dx, + offset.dy, + offset.dx, + offset.dy + caret.height, + TextDirection.ltr, + ) + ]; + } + + int isBaseShifted = 0; + bool isExtentShifted = false; + if (local.baseOffset == node.length - 1 && local.baseOffset > 0) { + // Since we exclude last line-break from rendered TextSpan we have to + // handle end-of-line selection explicitly. + local = local.copyWith(baseOffset: local.baseOffset - 1); + isBaseShifted = -1; + } else if (local.baseOffset == 0 && local.isCollapsed) { + // This takes care of beginning of line position. + local = local.copyWith(baseOffset: local.baseOffset + 1); + isBaseShifted = 1; + } + if (text.codeUnitAt(local.extentOffset - 1) == 0xA) { + // This takes care of the rest end-of-line scenarios, where there are + // actually line-breaks in the TextSpan (e.g. in code blocks). + local = local.copyWith(extentOffset: local.extentOffset + 1); + isExtentShifted = true; + } + final result = getBoxesForSelection(local).toList(); + if (isBaseShifted != 0) { + final box = result.first; + final dx = isBaseShifted == -1 ? box.right : box.left; + result.removeAt(0); + result.insert(0, + new ui.TextBox.fromLTRBD(dx, box.top, dx, box.bottom, box.direction)); + } + if (isExtentShifted) { + final box = result.last; + result.removeLast; + result.add(new ui.TextBox.fromLTRBD( + box.left, box.top, box.left, box.bottom, box.direction)); + } + return result; + } + + @override + void set text(InlineSpan value) { + _prototypePainter.text = new TextSpan(text: '.', style: value.style); + _selectionRects = null; + super.text = value; + } + + @override + void performLayout() { + super.performLayout(); + _prototypePainter.layout( + minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); + } + + @override + void paint(PaintingContext context, Offset offset) { + super.paint(context, offset); + } + + final TextPainter _prototypePainter; + List _selectionRects; + + /// Returns `true` if this paragraph intersects with document [selection]. + @override + bool intersectsWithSelection(TextSelection selection) { + final int base = node.documentOffset; + final int extent = base + node.length; + return base <= selection.extentOffset && selection.baseOffset <= extent; + } + + TextSelection _lastPaintedSelection; + @override + void paintSelection(PaintingContext context, Offset offset, + TextSelection selection, Color selectionColor) { + if (_lastPaintedSelection != selection) { + _selectionRects = null; + } + _selectionRects ??= getBoxesForSelection(getLocalSelection(selection)); + final Paint paint = new Paint()..color = selectionColor; + for (ui.TextBox box in _selectionRects) { + context.canvas.drawRect(box.toRect().shift(offset), paint); + } + _lastPaintedSelection = selection; + } +} diff --git a/zefyr/lib/src/widgets/scaffold.dart b/zefyr/lib/src/widgets/scaffold.dart new file mode 100644 index 00000000..f711070e --- /dev/null +++ b/zefyr/lib/src/widgets/scaffold.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +class ZefyrScaffold extends StatefulWidget { + final Widget child; + + const ZefyrScaffold({Key key, this.child}) : super(key: key); + + static ZefyrScaffoldState of(BuildContext context) { + final _ZefyrScaffoldAccess widget = + context.inheritFromWidgetOfExactType(_ZefyrScaffoldAccess); + return widget.scaffold; + } + + @override + ZefyrScaffoldState createState() => ZefyrScaffoldState(); +} + +class ZefyrScaffoldState extends State { + WidgetBuilder _toolbarBuilder; + + void showToolbar(WidgetBuilder builder) { + setState(() { + _toolbarBuilder = builder; + }); + } + + void hideToolbar() { + if (_toolbarBuilder != null) { + setState(() { + _toolbarBuilder = null; + }); + } + } + + @override + Widget build(BuildContext context) { + final toolbar = + (_toolbarBuilder == null) ? Container() : _toolbarBuilder(context); + return _ZefyrScaffoldAccess( + scaffold: this, + child: Column( + children: [ + Expanded(child: widget.child), + toolbar, + ], + ), + ); + } +} + +class _ZefyrScaffoldAccess extends InheritedWidget { + final ZefyrScaffoldState scaffold; + + _ZefyrScaffoldAccess({Widget child, this.scaffold}) : super(child: child); + + @override + bool updateShouldNotify(_ZefyrScaffoldAccess oldWidget) { + return oldWidget.scaffold != scaffold; + } +} diff --git a/zefyr/lib/src/widgets/scope.dart b/zefyr/lib/src/widgets/scope.dart new file mode 100644 index 00000000..2853a8a2 --- /dev/null +++ b/zefyr/lib/src/widgets/scope.dart @@ -0,0 +1,232 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:notus/notus.dart'; + +import 'controller.dart'; +import 'cursor_timer.dart'; +import 'editor.dart'; +import 'image.dart'; +import 'render_context.dart'; +import 'view.dart'; + +/// Provides access to shared state of [ZefyrEditor] or [ZefyrView]. +/// +/// A scope object can be created by an editable widget like [ZefyrEditor] in +/// which case it provides access to editing state, including focus nodes, +/// selection and such. Editable scope can be created using +/// [ZefyrScope.editable] constructor. +/// +/// If a scope object is created by a view-only widget like [ZefyrView] then +/// it only provides access to [imageDelegate]. +/// +/// Can be retrieved using [ZefyrScope.of]. +class ZefyrScope extends ChangeNotifier { + /// Creates a view-only scope. + /// + /// Normally used in [ZefyrView]. + ZefyrScope.view({@required ZefyrImageDelegate imageDelegate}) + : assert(imageDelegate != null), + isEditable = false, + _imageDelegate = imageDelegate; + + /// Creates editable scope. + /// + /// Normally used in [ZefyrEditor]. + ZefyrScope.editable({ + @required ZefyrController controller, + @required ZefyrImageDelegate imageDelegate, + @required FocusNode focusNode, + @required FocusScopeNode focusScope, + }) : assert(controller != null), + assert(imageDelegate != null), + assert(focusNode != null), + assert(focusScope != null), + isEditable = true, + _controller = controller, + _imageDelegate = imageDelegate, + _focusNode = focusNode, + _focusScope = focusScope, + _cursorTimer = CursorTimer(), + _renderContext = ZefyrRenderContext() { + _selectionStyle = _controller.getSelectionStyle(); + _selection = _controller.selection; + _controller.addListener(_handleControllerChange); + _focusNode.addListener(_handleFocusChange); + } + + static ZefyrScope of(BuildContext context) { + final ZefyrScopeAccess widget = + context.inheritFromWidgetOfExactType(ZefyrScopeAccess); + return widget.scope; + } + + ZefyrImageDelegate _imageDelegate; + ZefyrImageDelegate get imageDelegate => _imageDelegate; + set imageDelegate(ZefyrImageDelegate value) { + assert(value != null); + if (_imageDelegate != value) { + _imageDelegate = value; + notifyListeners(); + } + } + + ZefyrController _controller; + ZefyrController get controller => _controller; + set controller(ZefyrController value) { + assert(isEditable && value != null); + if (_controller != value) { + _controller.removeListener(_handleControllerChange); + _controller = value; + _selectionStyle = _controller.getSelectionStyle(); + _selection = _controller.selection; + _controller.addListener(_handleControllerChange); + notifyListeners(); + } + } + + FocusNode _focusNode; + FocusNode get focusNode => _focusNode; + set focusNode(FocusNode value) { + assert(isEditable && value != null); + if (_focusNode != value) { + _focusNode.removeListener(_handleFocusChange); + _focusNode = value; + _focusNode.addListener(_handleFocusChange); + notifyListeners(); + } + } + + FocusScopeNode _focusScope; + FocusScopeNode get focusScope => _focusScope; + set focusScope(FocusScopeNode value) { + assert(isEditable && value != null); + if (_focusScope != value) { + _focusScope = value; + } + } + + CursorTimer _cursorTimer; + CursorTimer get cursorTimer => _cursorTimer; + ValueNotifier get showCursor => cursorTimer.value; + + ZefyrRenderContext _renderContext; + ZefyrRenderContext get renderContext => _renderContext; + + NotusStyle get selectionStyle => _selectionStyle; + NotusStyle _selectionStyle; + TextSelection get selection => _selection; + TextSelection _selection; + + bool _disposed = false; + FocusNode _toolbarFocusNode; + + /// Whether this scope is backed by editable Zefyr widgets or read-only view. + /// + /// Returns `true` if this scope provides Zefyr interface that allows editing + /// (e.g. created by [ZefyrEditor]). Returns `false` if this scope provides + /// read-only view (e.g. created by [ZefyrView]). + /// + /// Editable scope provides access to corresponding [controller], [focusNode], + /// [focusScope], [showCursor], [renderContext] and other shared objects. For + /// non-editable scopes these are set to `null`. You can still access + /// objects which are not dependent on editing flow, e.g. [imageDelegate]. + final bool isEditable; + + set toolbarFocusNode(FocusNode node) { + assert(isEditable); + assert(!_disposed || node == null); + if (_toolbarFocusNode != node) { + _toolbarFocusNode?.removeListener(_handleFocusChange); + _toolbarFocusNode = node; + _toolbarFocusNode?.addListener(_handleFocusChange); + // We do not notify listeners here because it will happen when + // focus state changes, see [_handleFocusChange]. + } + } + + FocusOwner get focusOwner { + assert(isEditable); + assert(!_disposed); + if (_focusNode.hasFocus) { + return FocusOwner.editor; + } else if (_toolbarFocusNode?.hasFocus == true) { + return FocusOwner.toolbar; + } else { + return FocusOwner.none; + } + } + + void updateSelection(TextSelection value, + {ChangeSource source: ChangeSource.remote}) { + assert(isEditable); + assert(!_disposed); + _controller.updateSelection(value, source: source); + } + + void formatSelection(NotusAttribute value) { + assert(isEditable); + assert(!_disposed); + _controller.formatSelection(value); + } + + void focus() { + assert(isEditable); + assert(!_disposed); + _focusScope.requestFocus(_focusNode); + } + + void hideKeyboard() { + assert(isEditable); + assert(!_disposed); + _focusNode.unfocus(); + } + + @override + void dispose() { + assert(!_disposed); + _controller?.removeListener(_handleControllerChange); + _focusNode?.removeListener(_handleFocusChange); + _disposed = true; + super.dispose(); + } + + void _handleControllerChange() { + assert(!_disposed); + final attrs = _controller.getSelectionStyle(); + final selection = _controller.selection; + if (_selectionStyle != attrs || _selection != selection) { + _selectionStyle = attrs; + _selection = selection; + notifyListeners(); + } + } + + void _handleFocusChange() { + assert(!_disposed); + if (focusOwner == FocusOwner.none && !_selection.isCollapsed) { + // Collapse selection if there is nothing focused. + _controller.updateSelection(_selection.copyWith( + baseOffset: _selection.extentOffset, + extentOffset: _selection.extentOffset, + )); + } + notifyListeners(); + } + + @override + String toString() { + return '$ZefyrScope#${shortHash(this)}'; + } +} + +class ZefyrScopeAccess extends InheritedWidget { + final ZefyrScope scope; + + ZefyrScopeAccess({Key key, @required this.scope, @required Widget child}) + : super(key: key, child: child); + + @override + bool updateShouldNotify(ZefyrScopeAccess oldWidget) { + return scope != oldWidget.scope; + } +} diff --git a/zefyr/lib/src/widgets/selection.dart b/zefyr/lib/src/widgets/selection.dart new file mode 100644 index 00000000..febbd661 --- /dev/null +++ b/zefyr/lib/src/widgets/selection.dart @@ -0,0 +1,512 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:notus/notus.dart'; +import 'package:zefyr/util.dart'; + +import 'controller.dart'; +import 'editable_box.dart'; +import 'scope.dart'; + +RenderEditableBox _getEditableBox(HitTestResult result) { + for (var entry in result.path) { + if (entry.target is RenderEditableBox) { + return entry.target as RenderEditableBox; + } + } + return null; +} + +/// Selection overlay controls selection handles and other gestures. +class ZefyrSelectionOverlay extends StatefulWidget { + const ZefyrSelectionOverlay({ + Key key, + @required this.controller, + @required this.controls, + @required this.overlay, + }) : super(key: key); + + final ZefyrController controller; + final TextSelectionControls controls; + final OverlayState overlay; + + @override + _ZefyrSelectionOverlayState createState() => + new _ZefyrSelectionOverlayState(); +} + +class _ZefyrSelectionOverlayState extends State + implements TextSelectionDelegate { + @override + TextEditingValue get textEditingValue => + widget.controller.plainTextEditingValue; + + set textEditingValue(TextEditingValue value) { + final cursorPosition = value.selection.extentOffset; + final oldText = widget.controller.document.toPlainText(); + final newText = value.text; + final diff = fastDiff(oldText, newText, cursorPosition); + widget.controller.replaceText( + diff.start, diff.deleted.length, diff.inserted, + selection: value.selection); + } + + @override + void bringIntoView(ui.TextPosition position) { + // TODO: implement bringIntoView + } + + bool get isToolbarVisible => _toolbar != null; + bool get isToolbarHidden => _toolbar == null; + + @override + void hideToolbar() { + _didCaretTap = false; // reset double tap. + _toolbar?.remove(); + _toolbar = null; + _toolbarController.stop(); + } + + void showToolbar() { + final scope = ZefyrScope.of(context); + assert(scope != null); + final toolbarOpacity = _toolbarController.view; + _toolbar = new OverlayEntry( + builder: (context) => new FadeTransition( + opacity: toolbarOpacity, + child: new _SelectionToolbar( + scope: scope, + controls: widget.controls, + delegate: this, + ), + ), + ); + widget.overlay.insert(_toolbar); + _toolbarController.forward(from: 0.0); + } + + // + // Overridden members of State + // + + @override + void initState() { + super.initState(); + _toolbarController = new AnimationController( + duration: _kFadeDuration, vsync: widget.overlay); + } + + static const Duration _kFadeDuration = const Duration(milliseconds: 150); + + @override + void didUpdateWidget(ZefyrSelectionOverlay oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.overlay != widget.overlay) { + hideToolbar(); + _toolbarController.dispose(); + _toolbarController = new AnimationController( + duration: _kFadeDuration, vsync: widget.overlay); + } + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final editor = ZefyrScope.of(context); + if (_editor != editor) { + _editor?.removeListener(_handleChange); + _editor = editor; + _editor.addListener(_handleChange); + _selection = _editor.selection; + _focusOwner = _editor.focusOwner; + } + } + + @override + void dispose() { + _editor.removeListener(_handleChange); + hideToolbar(); + _toolbarController.dispose(); + _toolbarController = null; + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final overlay = new GestureDetector( + behavior: HitTestBehavior.translucent, + onTapDown: _handleTapDown, + onTap: _handleTap, + onTapCancel: _handleTapCancel, + onLongPress: _handleLongPress, + child: new Stack( + fit: StackFit.expand, + children: [ + new SelectionHandleDriver( + position: _SelectionHandlePosition.base, + controls: widget.controls, + ), + new SelectionHandleDriver( + position: _SelectionHandlePosition.extent, + controls: widget.controls, + ), + ], + ), + ); + return new Container(child: overlay); + } + + // + // Private members + // + + /// Global position of last TapDown event. + Offset _lastTapDownPosition; + + /// Global position of last TapDown which is potentially a long press. + Offset _longPressPosition; + + OverlayEntry _toolbar; + AnimationController _toolbarController; + + ZefyrScope _editor; + TextSelection _selection; + FocusOwner _focusOwner; + + bool _didCaretTap = false; + + void _handleChange() { + if (_selection != _editor.selection || _focusOwner != _editor.focusOwner) { + _updateToolbar(); + } + } + + void _updateToolbar() { + if (!mounted) { + return; + } + + final selection = _editor.selection; + final focusOwner = _editor.focusOwner; + setState(() { + if (focusOwner != FocusOwner.editor) { + hideToolbar(); + } else { + if (_selection != selection) { + if (selection.isCollapsed && isToolbarVisible) hideToolbar(); + _toolbar?.markNeedsBuild(); + if (!selection.isCollapsed && isToolbarHidden) showToolbar(); + } else { + if (!selection.isCollapsed && isToolbarHidden) { + showToolbar(); + } else if (isToolbarVisible) { + _toolbar?.markNeedsBuild(); + } + } + } + _selection = selection; + _focusOwner = focusOwner; + }); + } + + void _handleTapDown(TapDownDetails details) { + _lastTapDownPosition = details.globalPosition; + } + + void _handleTapCancel() { + // longPress arrives after tapCancel, so remember the tap position. + _longPressPosition = _lastTapDownPosition; + _lastTapDownPosition = null; + } + + void _handleTap() { + assert(_lastTapDownPosition != null); + final globalPoint = _lastTapDownPosition; + _lastTapDownPosition = null; + HitTestResult result = new HitTestResult(); + WidgetsBinding.instance.hitTest(result, globalPoint); + + RenderEditableProxyBox box = _getEditableBox(result); + if (box == null) { + box = _editor.renderContext.closestBoxForGlobalPoint(globalPoint); + } + if (box == null) return null; + + final localPoint = box.globalToLocal(globalPoint); + final position = box.getPositionForOffset(localPoint); + final selection = new TextSelection.collapsed( + offset: position.offset, + affinity: position.affinity, + ); + if (_didCaretTap && _selection == selection) { + _didCaretTap = false; + if (isToolbarVisible) { + hideToolbar(); + } else { + showToolbar(); + } + } else { + _didCaretTap = true; + } + widget.controller.updateSelection(selection, source: ChangeSource.local); + } + + void _handleLongPress() { + final Offset globalPoint = _longPressPosition; + _longPressPosition = null; + HitTestResult result = new HitTestResult(); + WidgetsBinding.instance.hitTest(result, globalPoint); + final box = _getEditableBox(result); + if (box == null) { + return; + } + final localPoint = box.globalToLocal(globalPoint); + final position = box.getPositionForOffset(localPoint); + final word = box.getWordBoundary(position); + final selection = new TextSelection( + baseOffset: word.start, + extentOffset: word.end, + ); + widget.controller.updateSelection(selection, source: ChangeSource.local); + } + + @override + bool get copyEnabled => true; + + @override + bool get cutEnabled => true; + + @override + bool get pasteEnabled => true; + + @override + bool get selectAllEnabled => true; +} + +enum _SelectionHandlePosition { base, extent } + +class SelectionHandleDriver extends StatefulWidget { + const SelectionHandleDriver({ + Key key, + @required this.position, + @required this.controls, + }) : super(key: key); + + final _SelectionHandlePosition position; + final TextSelectionControls controls; + + @override + _SelectionHandleDriverState createState() => + new _SelectionHandleDriverState(); +} + +class _SelectionHandleDriverState extends State { + ZefyrScope _scope; + + /// Current document selection. + TextSelection get selection => _selection; + TextSelection _selection; + + /// Returns `true` if this handle is located at the baseOffset of selection. + bool get isBaseHandle => widget.position == _SelectionHandlePosition.base; + + /// Character offset of this handle in the document. + /// + /// For base handle this equals to [TextSelection.baseOffset] and for + /// extent handle - [TextSelection.extentOffset]. + int get documentOffset => + isBaseHandle ? selection.baseOffset : selection.extentOffset; + + /// Position in pixels of this selection handle within its paragraph [block]. + Offset getPosition(RenderEditableBox block) { + if (block == null) return null; + + final localSelection = block.getLocalSelection(selection); + assert(localSelection != null); + + final boxes = block.getEndpointsForSelection(selection); + assert(boxes.isNotEmpty, 'Got empty boxes for selection ${selection}'); + + final box = isBaseHandle ? boxes.first : boxes.last; + final dx = isBaseHandle ? box.start : box.end; + return new Offset(dx, box.bottom); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final scope = ZefyrScope.of(context); + if (_scope != scope) { + _scope?.removeListener(_handleScopeChange); + _scope = scope; + _scope.addListener(_handleScopeChange); + } + _selection = _scope.selection; + } + + @override + void dispose() { + _scope?.removeListener(_handleScopeChange); + super.dispose(); + } + + // + // Overridden members + // + + @override + Widget build(BuildContext context) { + if (selection == null || + selection.isCollapsed || + widget.controls == null || + _scope.focusOwner != FocusOwner.editor) { + return new Container(); + } + final block = _scope.renderContext.boxForTextOffset(documentOffset); + final position = getPosition(block); + Widget handle; + if (position == null) { + handle = new Container(); + } else { + final handleType = isBaseHandle + ? TextSelectionHandleType.left + : TextSelectionHandleType.right; + handle = new Positioned( + left: position.dx, + top: position.dy, + child: widget.controls.buildHandle( + context, + handleType, + block.preferredLineHeight, + ), + ); + handle = new CompositedTransformFollower( + link: block.layerLink, + showWhenUnlinked: false, + child: new Stack( + overflow: Overflow.visible, + children: [handle], + ), + ); + } + // Always return this gesture detector even if handle is an empty container + // This way we prevent drag gesture from being canceled in case current + // position is somewhere outside of any visible paragraph block. + return new GestureDetector( + onPanStart: _handleDragStart, + onPanUpdate: _handleDragUpdate, + child: handle, + ); + } + + // + // Private members + // + + Offset _dragPosition; + + void _handleScopeChange() { + if (_selection != _scope.selection) { + setState(() { + _selection = _scope.selection; + }); + } + } + + void _handleDragStart(DragStartDetails details) { + _dragPosition = details.globalPosition; + } + + void _handleDragUpdate(DragUpdateDetails details) { + _dragPosition += details.delta; + final globalPoint = _dragPosition; + final paragraph = _scope.renderContext.boxForGlobalPoint(globalPoint); + if (paragraph == null) { + return; + } + + final localPoint = paragraph.globalToLocal(globalPoint); + final position = paragraph.getPositionForOffset(localPoint); + final newSelection = selection.copyWith( + baseOffset: isBaseHandle ? position.offset : selection.baseOffset, + extentOffset: isBaseHandle ? selection.extentOffset : position.offset, + ); + if (newSelection.baseOffset >= newSelection.extentOffset) { + // Don't allow reversed or collapsed selection. + return; + } + + if (newSelection != _selection) { + _scope.updateSelection(newSelection, source: ChangeSource.local); + } + } +} + +class _SelectionToolbar extends StatefulWidget { + const _SelectionToolbar({ + Key key, + @required this.scope, + @required this.controls, + @required this.delegate, + }) : super(key: key); + + final ZefyrScope scope; + final TextSelectionControls controls; + final TextSelectionDelegate delegate; + + @override + _SelectionToolbarState createState() => new _SelectionToolbarState(); +} + +class _SelectionToolbarState extends State<_SelectionToolbar> { + ZefyrScope get editable => widget.scope; + TextSelection get selection => widget.delegate.textEditingValue.selection; + + @override + Widget build(BuildContext context) { + return _buildToolbar(context); + } + + Widget _buildToolbar(BuildContext context) { + final base = selection.baseOffset; + // TODO: Editable is not refreshed and may contain stale renderContext instance. + final block = editable.renderContext.boxForTextOffset(base); + if (block == null) { + return Container(); + } + final boxes = block.getEndpointsForSelection(selection); + // Find the horizontal midpoint, just above the selected text. + final Offset midpoint = new Offset( + (boxes.length == 1) + ? (boxes[0].start + boxes[0].end) / 2.0 + : (boxes[0].start + boxes[1].start) / 2.0, + boxes[0].bottom - block.preferredLineHeight, + ); + + final Rect editingRegion = new Rect.fromPoints( + block.localToGlobal(Offset.zero), + block.localToGlobal(block.size.bottomRight(Offset.zero)), + ); +// final toolbar = widget.controls +// .buildToolbar(context, editingRegion, midpoint, widget.delegate); + final Offset endpoint = new Offset( + (boxes.length == 1) + ? (boxes[0].start + boxes[0].end) + : (boxes[0].start + boxes[1].start), + boxes[0].bottom - block.preferredLineHeight, + ); + final TextSelectionPoint textEndpoint = new TextSelectionPoint(endpoint, TextDirection.ltr); + final toolbar = widget.controls + .buildToolbar(context, editingRegion,0.0, midpoint, [textEndpoint], widget.delegate); + return new CompositedTransformFollower( + link: block.layerLink, + showWhenUnlinked: false, + offset: -editingRegion.topLeft, + child: toolbar, + ); + } +} diff --git a/zefyr/lib/src/widgets/theme.dart b/zefyr/lib/src/widgets/theme.dart new file mode 100644 index 00000000..0f7c1160 --- /dev/null +++ b/zefyr/lib/src/widgets/theme.dart @@ -0,0 +1,312 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; + +/// Applies a Zefyr editor theme to descendant widgets. +/// +/// Describes colors and typographic styles for an editor. +/// +/// Descendant widgets obtain the current theme's [ZefyrThemeData] object using +/// [ZefyrTheme.of]. +/// +/// See also: +/// +/// * [ZefyrThemeData], which describes actual configuration of a theme. +class ZefyrTheme extends InheritedWidget { + final ZefyrThemeData data; + + /// Applies the given theme [data] to [child]. + /// + /// The [data] and [child] arguments must not be null. + ZefyrTheme({ + Key key, + @required this.data, + @required Widget child, + }) : assert(data != null), + assert(child != null), + super(key: key, child: child); + + @override + bool updateShouldNotify(ZefyrTheme oldWidget) { + return data != oldWidget.data; + } + + /// The data from the closest [ZefyrTheme] instance that encloses the given + /// context. + /// + /// Returns `null` if there is no [ZefyrTheme] in the given build context + /// and [nullOk] is set to `true`. If [nullOk] is set to `false` (default) + /// then this method asserts. + static ZefyrThemeData of(BuildContext context, {bool nullOk: false}) { + final ZefyrTheme widget = context.inheritFromWidgetOfExactType(ZefyrTheme); + if (widget == null && nullOk) return null; + assert(widget != null, + '$ZefyrTheme.of() called with a context that does not contain a ZefyrEditor.'); + return widget.data; + } +} + +/// Holds colors and typography styles for [ZefyrEditor]. +class ZefyrThemeData { + final TextStyle boldStyle; + final TextStyle italicStyle; + final TextStyle linkStyle; + final StyleTheme paragraphTheme; + final HeadingTheme headingTheme; + final BlockTheme blockTheme; + final Color selectionColor; + final Color cursorColor; + + /// Size of indentation for blocks. + final double indentSize; + final ZefyrToolbarTheme toolbarTheme; + + factory ZefyrThemeData.fallback(BuildContext context) { + final defaultStyle = DefaultTextStyle.of(context); + final paragraphStyle = defaultStyle.style.copyWith( + fontSize: 16.0, + height: 1.25, + fontWeight: FontWeight.normal, + color: Colors.grey.shade800, + ); + final padding = const EdgeInsets.only(bottom: 16.0); + final boldStyle = new TextStyle(fontWeight: FontWeight.bold); + final italicStyle = new TextStyle(fontStyle: FontStyle.italic); + final linkStyle = + TextStyle(color: Colors.blue, decoration: TextDecoration.underline); + + return new ZefyrThemeData( + boldStyle: boldStyle, + italicStyle: italicStyle, + linkStyle: linkStyle, + paragraphTheme: + new StyleTheme(textStyle: paragraphStyle, padding: padding), + headingTheme: new HeadingTheme.fallback(), + blockTheme: new BlockTheme.fallback(), + selectionColor: Colors.lightBlueAccent.shade100, + cursorColor: Colors.black, + indentSize: 16.0, + toolbarTheme: new ZefyrToolbarTheme.fallback(context), + ); + } + + const ZefyrThemeData({ + this.boldStyle, + this.italicStyle, + this.linkStyle, + this.paragraphTheme, + this.headingTheme, + this.blockTheme, + this.selectionColor, + this.cursorColor, + this.indentSize, + this.toolbarTheme, + }); + + ZefyrThemeData copyWith({ + TextStyle textStyle, + TextStyle boldStyle, + TextStyle italicStyle, + TextStyle linkStyle, + StyleTheme paragraphTheme, + HeadingTheme headingTheme, + BlockTheme blockTheme, + Color selectionColor, + Color cursorColor, + double indentSize, + ZefyrToolbarTheme toolbarTheme, + }) { + return new ZefyrThemeData( + boldStyle: boldStyle ?? this.boldStyle, + italicStyle: italicStyle ?? this.italicStyle, + linkStyle: linkStyle ?? this.linkStyle, + paragraphTheme: paragraphTheme ?? this.paragraphTheme, + headingTheme: headingTheme ?? this.headingTheme, + blockTheme: blockTheme ?? this.blockTheme, + selectionColor: selectionColor ?? this.selectionColor, + cursorColor: cursorColor ?? this.cursorColor, + indentSize: indentSize ?? this.indentSize, + toolbarTheme: toolbarTheme ?? this.toolbarTheme, + ); + } + + ZefyrThemeData merge(ZefyrThemeData other) { + return copyWith( + boldStyle: other.boldStyle, + italicStyle: other.italicStyle, + linkStyle: other.linkStyle, + paragraphTheme: other.paragraphTheme, + headingTheme: other.headingTheme, + blockTheme: other.blockTheme, + selectionColor: other.selectionColor, + cursorColor: other.cursorColor, + indentSize: other.indentSize, + toolbarTheme: other.toolbarTheme, + ); + } +} + +/// Theme for heading-styled lines of text. +class HeadingTheme { + /// Style theme for level 1 headings. + final StyleTheme level1; + + /// Style theme for level 2 headings. + final StyleTheme level2; + + /// Style theme for level 3 headings. + final StyleTheme level3; + + HeadingTheme({ + @required this.level1, + @required this.level2, + @required this.level3, + }); + + /// Creates fallback theme for headings. + factory HeadingTheme.fallback() { + return HeadingTheme( + level1: StyleTheme( + textStyle: TextStyle( + fontSize: 30.0, + color: Colors.grey.shade800, + height: 1.25, + fontWeight: FontWeight.w600, + ), + padding: EdgeInsets.only(top: 16.0, bottom: 16.0), + ), + level2: StyleTheme( + textStyle: TextStyle( + fontSize: 24.0, + color: Colors.grey.shade800, + height: 1.25, + fontWeight: FontWeight.w600, + ), + padding: EdgeInsets.only(bottom: 8.0, top: 8.0), + ), + level3: StyleTheme( + textStyle: TextStyle( + fontSize: 20.0, + color: Colors.grey.shade800, + height: 1.25, + fontWeight: FontWeight.w600, + ), + padding: EdgeInsets.only(bottom: 8.0, top: 8.0), + ), + ); + } +} + +/// Theme for a block of lines in a document. +class BlockTheme { + /// Style theme for bullet lists. + final StyleTheme bulletList; + + /// Style theme for number lists. + final StyleTheme numberList; + + /// Style theme for code snippets. + final StyleTheme code; + + /// Style theme for quotes. + final StyleTheme quote; + + BlockTheme({ + @required this.bulletList, + @required this.numberList, + @required this.quote, + @required this.code, + }); + + /// Creates fallback theme for blocks. + factory BlockTheme.fallback() { + final padding = const EdgeInsets.only(bottom: 8.0); + return new BlockTheme( + bulletList: new StyleTheme(padding: padding), + numberList: new StyleTheme(padding: padding), + quote: new StyleTheme( + textStyle: new TextStyle(color: Colors.grey.shade700), + padding: padding, + ), + code: new StyleTheme( + textStyle: new TextStyle( + color: Colors.blueGrey.shade800, + fontFamily: Platform.isIOS ? 'Menlo' : 'Roboto Mono', + fontSize: 14.0, + height: 1.25, + ), + padding: padding, + ), + ); + } +} + +/// Theme for a specific attribute style. +/// +/// Used in [HeadingTheme] and [BlockTheme], as well as in +/// [ZefyrThemeData.paragraphTheme]. +class StyleTheme { + /// Text style of this theme. + final TextStyle textStyle; + + /// Padding to apply around lines of text. + final EdgeInsets padding; + + /// Creates a new [StyleTheme]. + StyleTheme({ + this.textStyle, + this.padding, + }); +} + +/// Defines styles and colors for [ZefyrToolbar]. +class ZefyrToolbarTheme { + /// The background color of toolbar. + final Color color; + + /// Color of buttons in toggled state. + final Color toggleColor; + + /// Color of button icons. + final Color iconColor; + + /// Color of button icons in disabled state. + final Color disabledIconColor; + + /// Creates fallback theme for editor toolbars. + factory ZefyrToolbarTheme.fallback(BuildContext context) { + final theme = Theme.of(context); + return ZefyrToolbarTheme._( + color: theme.primaryColorLight, + toggleColor: theme.primaryColor, + iconColor: theme.primaryIconTheme.color, + disabledIconColor: theme.primaryColor, + ); + } + + ZefyrToolbarTheme._({ + @required this.color, + @required this.toggleColor, + @required this.iconColor, + @required this.disabledIconColor, + }); + + ZefyrToolbarTheme copyWith({ + Color color, + Color toggleColor, + Color iconColor, + Color disabledIconColor, + }) { + return ZefyrToolbarTheme._( + color: color ?? this.color, + toggleColor: toggleColor ?? this.toggleColor, + iconColor: iconColor ?? this.iconColor, + disabledIconColor: disabledIconColor ?? this.disabledIconColor, + ); + } +} diff --git a/zefyr/lib/src/widgets/toolbar.dart b/zefyr/lib/src/widgets/toolbar.dart new file mode 100644 index 00000000..d9364cbf --- /dev/null +++ b/zefyr/lib/src/widgets/toolbar.dart @@ -0,0 +1,398 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:notus/notus.dart'; + +import 'buttons.dart'; +import 'scope.dart'; +import 'theme.dart'; + +/// List of all button actions supported by [ZefyrToolbar] buttons. +enum ZefyrToolbarAction { + bold, + italic, + link, + unlink, + clipboardCopy, + openInBrowser, + heading, + headingLevel1, + headingLevel2, + headingLevel3, + bulletList, + numberList, + code, + quote, + horizontalRule, + image, + cameraImage, + galleryImage, + hideKeyboard, + close, + confirm, +} + +final kZefyrToolbarAttributeActions = { + ZefyrToolbarAction.bold: NotusAttribute.bold, + ZefyrToolbarAction.italic: NotusAttribute.italic, + ZefyrToolbarAction.link: NotusAttribute.link, + ZefyrToolbarAction.heading: NotusAttribute.heading, + ZefyrToolbarAction.headingLevel1: NotusAttribute.heading.level1, + ZefyrToolbarAction.headingLevel2: NotusAttribute.heading.level2, + ZefyrToolbarAction.headingLevel3: NotusAttribute.heading.level3, + ZefyrToolbarAction.bulletList: NotusAttribute.block.bulletList, + ZefyrToolbarAction.numberList: NotusAttribute.block.numberList, + ZefyrToolbarAction.code: NotusAttribute.block.code, + ZefyrToolbarAction.quote: NotusAttribute.block.quote, + ZefyrToolbarAction.horizontalRule: NotusAttribute.embed.horizontalRule, +}; + +/// Allows customizing appearance of [ZefyrToolbar]. +abstract class ZefyrToolbarDelegate { + /// Builds toolbar button for specified [action]. + /// + /// Returned widget is usually an instance of [ZefyrButton]. + Widget buildButton(BuildContext context, ZefyrToolbarAction action, + {VoidCallback onPressed}); +} + +/// Scaffold for [ZefyrToolbar]. +class ZefyrToolbarScaffold extends StatelessWidget { + const ZefyrToolbarScaffold({ + Key key, + @required this.body, + this.trailing, + this.autoImplyTrailing: true, + }) : super(key: key); + + final Widget body; + final Widget trailing; + final bool autoImplyTrailing; + + @override + Widget build(BuildContext context) { + final theme = ZefyrTheme.of(context).toolbarTheme; + final toolbar = ZefyrToolbar.of(context); + final constraints = + BoxConstraints.tightFor(height: ZefyrToolbar.kToolbarHeight); + final children = [ + Expanded(child: body), + ]; + + if (trailing != null) { + children.add(trailing); + } else if (autoImplyTrailing) { + children.add(toolbar.buildButton(context, ZefyrToolbarAction.close)); + } + return new Container( + constraints: constraints, + child: Material(color: theme.color, child: Row(children: children)), + ); + } +} + +/// Toolbar for [ZefyrEditor]. +class ZefyrToolbar extends StatefulWidget implements PreferredSizeWidget { + static const kToolbarHeight = 50.0; + + const ZefyrToolbar({ + Key key, + @required this.editor, + this.autoHide: true, + this.delegate, + }) : super(key: key); + + final ZefyrToolbarDelegate delegate; + final ZefyrScope editor; + + /// Whether to automatically hide this toolbar when editor loses focus. + final bool autoHide; + + static ZefyrToolbarState of(BuildContext context) { + final _ZefyrToolbarScope scope = + context.inheritFromWidgetOfExactType(_ZefyrToolbarScope); + return scope?.toolbar; + } + + @override + ZefyrToolbarState createState() => ZefyrToolbarState(); + + @override + ui.Size get preferredSize => new Size.fromHeight(ZefyrToolbar.kToolbarHeight); +} + +class _ZefyrToolbarScope extends InheritedWidget { + _ZefyrToolbarScope({Key key, @required Widget child, @required this.toolbar}) + : super(key: key, child: child); + + final ZefyrToolbarState toolbar; + + @override + bool updateShouldNotify(_ZefyrToolbarScope oldWidget) { + return toolbar != oldWidget.toolbar; + } +} + +class ZefyrToolbarState extends State + with SingleTickerProviderStateMixin { + final Key _toolbarKey = UniqueKey(); + final Key _overlayKey = UniqueKey(); + + ZefyrToolbarDelegate _delegate; + AnimationController _overlayAnimation; + WidgetBuilder _overlayBuilder; + Completer _overlayCompleter; + + TextSelection _selection; + + void markNeedsRebuild() { + setState(() { + if (_selection != editor.selection) { + _selection = editor.selection; + closeOverlay(); + } + }); + } + + Widget buildButton(BuildContext context, ZefyrToolbarAction action, + {VoidCallback onPressed}) { + return _delegate.buildButton(context, action, onPressed: onPressed); + } + + Future showOverlay(WidgetBuilder builder) async { + assert(_overlayBuilder == null); + final completer = new Completer(); + setState(() { + _overlayBuilder = builder; + _overlayCompleter = completer; + _overlayAnimation.forward(); + }); + return completer.future; + } + + void closeOverlay() { + if (!hasOverlay) return; + _overlayAnimation.reverse().whenComplete(() { + setState(() { + _overlayBuilder = null; + _overlayCompleter?.complete(); + _overlayCompleter = null; + }); + }); + } + + bool get hasOverlay => _overlayBuilder != null; + + ZefyrScope get editor => widget.editor; + + @override + void initState() { + super.initState(); + _delegate = widget.delegate ?? new _DefaultZefyrToolbarDelegate(); + _overlayAnimation = new AnimationController( + vsync: this, duration: Duration(milliseconds: 100)); + _selection = editor.selection; + } + + @override + void didUpdateWidget(ZefyrToolbar oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.delegate != oldWidget.delegate) { + _delegate = widget.delegate ?? new _DefaultZefyrToolbarDelegate(); + } + } + + @override + void dispose() { + _overlayAnimation.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final layers = []; + + // Must set unique key for the toolbar to prevent it from reconstructing + // new state each time we toggle overlay. + final toolbar = ZefyrToolbarScaffold( + key: _toolbarKey, + body: ZefyrButtonList(buttons: _buildButtons(context)), + trailing: buildButton(context, ZefyrToolbarAction.hideKeyboard), + ); + + layers.add(toolbar); + + if (hasOverlay) { + Widget widget = new Builder(builder: _overlayBuilder); + assert(widget != null); + final overlay = FadeTransition( + key: _overlayKey, + opacity: _overlayAnimation, + child: widget, + ); + layers.add(overlay); + } + + final constraints = + BoxConstraints.tightFor(height: ZefyrToolbar.kToolbarHeight); + return _ZefyrToolbarScope( + toolbar: this, + child: Container( + constraints: constraints, + child: Stack(children: layers), + ), + ); + } + + List _buildButtons(BuildContext context) { + final buttons = [ + buildButton(context, ZefyrToolbarAction.bold), + buildButton(context, ZefyrToolbarAction.italic), + LinkButton(), + HeadingButton(), + buildButton(context, ZefyrToolbarAction.bulletList), + buildButton(context, ZefyrToolbarAction.numberList), + buildButton(context, ZefyrToolbarAction.quote), + buildButton(context, ZefyrToolbarAction.code), + buildButton(context, ZefyrToolbarAction.horizontalRule), + ImageButton(), + ]; + return buttons; + } +} + +/// Scrollable list of toolbar buttons. +class ZefyrButtonList extends StatefulWidget { + const ZefyrButtonList({Key key, @required this.buttons}) : super(key: key); + final List buttons; + + @override + _ZefyrButtonListState createState() => _ZefyrButtonListState(); +} + +class _ZefyrButtonListState extends State { + final ScrollController _controller = new ScrollController(); + bool _showLeftArrow = false; + bool _showRightArrow = false; + + @override + void initState() { + super.initState(); + _controller.addListener(_handleScroll); + // Workaround to allow scroll controller attach to our ListView so that + // we can detect if overflow arrows need to be shown on init. + // TODO: find a better way to detect overflow + Timer.run(_handleScroll); + } + + @override + Widget build(BuildContext context) { + final theme = ZefyrTheme.of(context).toolbarTheme; + final color = theme.iconColor; + final list = ListView( + scrollDirection: Axis.horizontal, + controller: _controller, + children: widget.buttons, + physics: ClampingScrollPhysics(), + ); + + final leftArrow = _showLeftArrow + ? Icon(Icons.arrow_left, size: 18.0, color: color) + : null; + final rightArrow = _showRightArrow + ? Icon(Icons.arrow_right, size: 18.0, color: color) + : null; + return Row( + children: [ + SizedBox( + width: 12.0, + height: ZefyrToolbar.kToolbarHeight, + child: Container(child: leftArrow, color: theme.color), + ), + Expanded(child: ClipRect(child: list)), + SizedBox( + width: 12.0, + height: ZefyrToolbar.kToolbarHeight, + child: Container(child: rightArrow, color: theme.color), + ), + ], + ); + } + + void _handleScroll() { + setState(() { + _showLeftArrow = + _controller.position.minScrollExtent != _controller.position.pixels; + _showRightArrow = + _controller.position.maxScrollExtent != _controller.position.pixels; + }); + } +} + +class _DefaultZefyrToolbarDelegate implements ZefyrToolbarDelegate { + static const kDefaultButtonIcons = { + ZefyrToolbarAction.bold: Icons.format_bold, + ZefyrToolbarAction.italic: Icons.format_italic, + ZefyrToolbarAction.link: Icons.link, + ZefyrToolbarAction.unlink: Icons.link_off, + ZefyrToolbarAction.clipboardCopy: Icons.content_copy, + ZefyrToolbarAction.openInBrowser: Icons.open_in_new, + ZefyrToolbarAction.heading: Icons.format_size, + ZefyrToolbarAction.bulletList: Icons.format_list_bulleted, + ZefyrToolbarAction.numberList: Icons.format_list_numbered, + ZefyrToolbarAction.code: Icons.code, + ZefyrToolbarAction.quote: Icons.format_quote, + ZefyrToolbarAction.horizontalRule: Icons.remove, + ZefyrToolbarAction.image: Icons.photo, + ZefyrToolbarAction.cameraImage: Icons.photo_camera, + ZefyrToolbarAction.galleryImage: Icons.photo_library, + ZefyrToolbarAction.hideKeyboard: Icons.keyboard_hide, + ZefyrToolbarAction.close: Icons.close, + ZefyrToolbarAction.confirm: Icons.check, + }; + + static const kSpecialIconSizes = { + ZefyrToolbarAction.unlink: 20.0, + ZefyrToolbarAction.clipboardCopy: 20.0, + ZefyrToolbarAction.openInBrowser: 20.0, + ZefyrToolbarAction.close: 20.0, + ZefyrToolbarAction.confirm: 20.0, + }; + + static const kDefaultButtonTexts = { + ZefyrToolbarAction.headingLevel1: 'H1', + ZefyrToolbarAction.headingLevel2: 'H2', + ZefyrToolbarAction.headingLevel3: 'H3', + }; + + @override + Widget buildButton(BuildContext context, ZefyrToolbarAction action, + {VoidCallback onPressed}) { + final theme = Theme.of(context); + if (kDefaultButtonIcons.containsKey(action)) { + final icon = kDefaultButtonIcons[action]; + final size = kSpecialIconSizes[action]; + return ZefyrButton.icon( + action: action, + icon: icon, + iconSize: size, + onPressed: onPressed, + ); + } else { + final text = kDefaultButtonTexts[action]; + assert(text != null); + final style = theme.textTheme.caption + .copyWith(fontWeight: FontWeight.bold, fontSize: 14.0); + return ZefyrButton.text( + action: action, + text: text, + style: style, + onPressed: onPressed, + ); + } + } +} diff --git a/zefyr/lib/src/widgets/view.dart b/zefyr/lib/src/widgets/view.dart new file mode 100644 index 00000000..d7b7b13e --- /dev/null +++ b/zefyr/lib/src/widgets/view.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:meta/meta.dart'; +import 'package:notus/notus.dart'; + +import 'code.dart'; +import 'common.dart'; +import 'image.dart'; +import 'list.dart'; +import 'paragraph.dart'; +import 'quote.dart'; +import 'scope.dart'; +import 'theme.dart'; + +/// Non-scrollable read-only view of Notus rich text documents. +@experimental +class ZefyrView extends StatefulWidget { + final NotusDocument document; + final ZefyrImageDelegate imageDelegate; + + const ZefyrView({Key key, @required this.document, this.imageDelegate}) + : super(key: key); + + @override + ZefyrViewState createState() => ZefyrViewState(); +} + +class ZefyrViewState extends State { + ZefyrScope _scope; + ZefyrThemeData _themeData; + + ZefyrImageDelegate get imageDelegate => widget.imageDelegate; + + @override + void initState() { + super.initState(); + _scope = ZefyrScope.view(imageDelegate: widget.imageDelegate); + } + + @override + void didUpdateWidget(ZefyrView oldWidget) { + super.didUpdateWidget(oldWidget); + _scope.imageDelegate = widget.imageDelegate; + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final parentTheme = ZefyrTheme.of(context, nullOk: true); + final fallbackTheme = ZefyrThemeData.fallback(context); + _themeData = (parentTheme != null) + ? fallbackTheme.merge(parentTheme) + : fallbackTheme; + } + + @override + void dispose() { + _scope.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ZefyrTheme( + data: _themeData, + child: ZefyrScopeAccess( + scope: _scope, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: _buildChildren(context), + ), + ), + ); + } + + List _buildChildren(BuildContext context) { + final result = []; + for (var node in widget.document.root.children) { + result.add(_defaultChildBuilder(context, node)); + } + return result; + } + + Widget _defaultChildBuilder(BuildContext context, Node node) { + if (node is LineNode) { + if (node.hasEmbed) { + return new RawZefyrLine(node: node); + } else if (node.style.contains(NotusAttribute.heading)) { + return new ZefyrHeading(node: node); + } + return new ZefyrParagraph(node: node); + } + + final BlockNode block = node; + final blockStyle = block.style.get(NotusAttribute.block); + if (blockStyle == NotusAttribute.block.code) { + return new ZefyrCode(node: block); + } else if (blockStyle == NotusAttribute.block.bulletList) { + return new ZefyrList(node: block); + } else if (blockStyle == NotusAttribute.block.numberList) { + return new ZefyrList(node: block); + } else if (blockStyle == NotusAttribute.block.quote) { + return new ZefyrQuote(node: block); + } + + throw new UnimplementedError('Block format $blockStyle.'); + } +} diff --git a/zefyr/lib/util.dart b/zefyr/lib/util.dart new file mode 100644 index 00000000..7ef640e8 --- /dev/null +++ b/zefyr/lib/util.dart @@ -0,0 +1,40 @@ +// Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Utility functions for Zefyr. +library zefyr.util; + +import 'dart:math' as math; + +import 'package:quill_delta/quill_delta.dart'; + +export 'src/fast_diff.dart'; + +int getPositionDelta(Delta user, Delta actual) { + final userIter = new DeltaIterator(user); + final actualIter = new DeltaIterator(actual); + int diff = 0; + while (userIter.hasNext || actualIter.hasNext) { + num length = math.min(userIter.peekLength(), actualIter.peekLength()); + final userOp = userIter.next(length); + final actualOp = actualIter.next(length); + assert(userOp.length == actualOp.length); + if (userOp.key == actualOp.key) continue; + if (userOp.isInsert && actualOp.isRetain) { + diff -= userOp.length; + } else if (userOp.isDelete && actualOp.isRetain) { + diff += userOp.length; + } else if (userOp.isRetain && actualOp.isInsert) { + if (actualOp.data.startsWith('\n') ) { + // At this point user input reached its end (retain). If a heuristic + // rule inserts a new line we should keep cursor on it's original position. + continue; + } + diff += actualOp.length; + } else { + // TODO: this likely needs to cover more edge cases. + } + } + return diff; +} diff --git a/zefyr/lib/zefyr.dart b/zefyr/lib/zefyr.dart new file mode 100644 index 00000000..846ba5f5 --- /dev/null +++ b/zefyr/lib/zefyr.dart @@ -0,0 +1,22 @@ +library zefyr; + +export 'package:notus/notus.dart'; + +export 'src/widgets/buttons.dart' hide HeadingButton, LinkButton; +export 'src/widgets/code.dart'; +export 'src/widgets/common.dart'; +export 'src/widgets/controller.dart'; +export 'src/widgets/editable_text.dart'; +export 'src/widgets/editor.dart'; +export 'src/widgets/field.dart'; +export 'src/widgets/horizontal_rule.dart'; +export 'src/widgets/image.dart'; +export 'src/widgets/list.dart'; +export 'src/widgets/paragraph.dart'; +export 'src/widgets/quote.dart'; +export 'src/widgets/scaffold.dart'; +export 'src/widgets/scope.dart' hide ZefyrScopeAccess; +export 'src/widgets/selection.dart' hide SelectionHandleDriver; +export 'src/widgets/theme.dart'; +export 'src/widgets/toolbar.dart'; +export 'src/widgets/view.dart'; \ No newline at end of file diff --git a/zefyr/pubspec.yaml b/zefyr/pubspec.yaml new file mode 100644 index 00000000..24625589 --- /dev/null +++ b/zefyr/pubspec.yaml @@ -0,0 +1,59 @@ +name: zefyr +description: A new Flutter package project. +version: 0.0.1 +author: Anatoly Pulyaevskiy +homepage: + +environment: + sdk: ">=2.1.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + collection: ^1.14.6 + url_launcher: ^5.0.0 + image_picker: ^0.5.0 + quill_delta: ^1.0.0-dev.1.0 + notus: ^0.1.0 + meta: ^1.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/zefyr/test/zefyr_test.dart b/zefyr/test/zefyr_test.dart new file mode 100644 index 00000000..f9502254 --- /dev/null +++ b/zefyr/test/zefyr_test.dart @@ -0,0 +1,13 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:zefyr/zefyr.dart'; + +void main() { +// test('adds one to input values', () { +// final calculator = Calculator(); +// expect(calculator.addOne(2), 3); +// expect(calculator.addOne(-7), -6); +// expect(calculator.addOne(0), 1); +// expect(() => calculator.addOne(null), throwsNoSuchMethodError); +// }); +}