├── .gitignore
├── .idea
├── .gitignore
├── compiler.xml
├── deploymentTargetDropDown.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── misc.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── libs
│ ├── richtext-commonmark-release.aar
│ ├── richtext-ui-material3-release.aar
│ └── richtext-ui-release.aar
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── equationl
│ │ └── giteetodo
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── java
│ │ └── com
│ │ │ └── equationl
│ │ │ └── giteetodo
│ │ │ ├── App.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── constants
│ │ │ ├── ChooseRepoType.kt
│ │ │ ├── ClientInfo.kt
│ │ │ ├── DefaultText.kt
│ │ │ ├── IntentDataKey.kt
│ │ │ ├── Net.kt
│ │ │ └── ShareElementKey.kt
│ │ │ ├── data
│ │ │ ├── DataBaseModule.kt
│ │ │ ├── NetWorkMoudle.kt
│ │ │ ├── auth
│ │ │ │ ├── OAuthApi.kt
│ │ │ │ └── model
│ │ │ │ │ └── response
│ │ │ │ │ └── Token.kt
│ │ │ ├── repos
│ │ │ │ ├── RepoApi.kt
│ │ │ │ ├── db
│ │ │ │ │ ├── IssueConverters.kt
│ │ │ │ │ ├── IssueDao.kt
│ │ │ │ │ ├── IssueDb.kt
│ │ │ │ │ └── IssueRemoteKeyDao.kt
│ │ │ │ ├── model
│ │ │ │ │ ├── common
│ │ │ │ │ │ ├── IssueRemoteKey.kt
│ │ │ │ │ │ └── TodoShowData.kt
│ │ │ │ │ ├── request
│ │ │ │ │ │ ├── CreateComment.kt
│ │ │ │ │ │ ├── CreateIssues.kt
│ │ │ │ │ │ ├── CreateLabel.kt
│ │ │ │ │ │ ├── UpdateContent.kt
│ │ │ │ │ │ └── UpdateIssue.kt
│ │ │ │ │ └── response
│ │ │ │ │ │ ├── Comments.kt
│ │ │ │ │ │ ├── Contents.kt
│ │ │ │ │ │ ├── Issue.kt
│ │ │ │ │ │ ├── Label.kt
│ │ │ │ │ │ ├── SinpleUser.kt
│ │ │ │ │ │ └── UpdateContent.kt
│ │ │ │ └── paging
│ │ │ │ │ ├── pagingSource
│ │ │ │ │ └── ReposPagingSource.kt
│ │ │ │ │ └── remoteMediator
│ │ │ │ │ └── IssueRemoteMediator.kt
│ │ │ └── user
│ │ │ │ ├── UserApi.kt
│ │ │ │ └── model
│ │ │ │ ├── request
│ │ │ │ └── UserRepos.kt
│ │ │ │ └── response
│ │ │ │ ├── Repo.kt
│ │ │ │ └── User.kt
│ │ │ ├── ui
│ │ │ ├── Home.kt
│ │ │ ├── common
│ │ │ │ ├── Direction.kt
│ │ │ │ ├── IssueState.kt
│ │ │ │ └── Route.kt
│ │ │ ├── page
│ │ │ │ ├── About.kt
│ │ │ │ ├── Image.kt
│ │ │ │ ├── LabelManager.kt
│ │ │ │ ├── Login.kt
│ │ │ │ ├── LoginOAuth.kt
│ │ │ │ ├── Profile.kt
│ │ │ │ ├── RepoDetail.kt
│ │ │ │ ├── RepoList.kt
│ │ │ │ ├── Setting.kt
│ │ │ │ ├── TodoDetail.kt
│ │ │ │ ├── TodoHome.kt
│ │ │ │ └── TodoList.kt
│ │ │ ├── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Shape.kt
│ │ │ │ ├── Theme.kt
│ │ │ │ └── Type.kt
│ │ │ └── widgets
│ │ │ │ ├── BlurIMage.kt
│ │ │ │ ├── CommonMarkDown.kt
│ │ │ │ ├── CommonTip.kt
│ │ │ │ ├── Dialog.kt
│ │ │ │ ├── ExpandableItem.kt
│ │ │ │ ├── Extend.kt
│ │ │ │ ├── LinkText.kt
│ │ │ │ ├── TopBar.kt
│ │ │ │ ├── WebView.kt
│ │ │ │ └── placeholder
│ │ │ │ ├── Placeholder.kt
│ │ │ │ ├── PlaceholderHighlight.kt
│ │ │ │ └── material3
│ │ │ │ ├── Placeholder.kt
│ │ │ │ └── PlaceholderHighlight.kt
│ │ │ ├── util
│ │ │ ├── Coil.kt
│ │ │ ├── FileUtils.kt
│ │ │ ├── JsonUtils.kt
│ │ │ ├── Utils.kt
│ │ │ ├── Widget.kt
│ │ │ ├── datastore
│ │ │ │ ├── DataKey.kt
│ │ │ │ └── DataStoreUtils.kt
│ │ │ └── event
│ │ │ │ ├── EventKey.kt
│ │ │ │ └── FlowBus.kt
│ │ │ ├── viewmodel
│ │ │ ├── LabelMgViewModel.kt
│ │ │ ├── LoginOauthViewModel.kt
│ │ │ ├── LoginViewModel.kt
│ │ │ ├── ProfileViewModel.kt
│ │ │ ├── RepoDetailViewModel.kt
│ │ │ ├── RepoListViewModel.kt
│ │ │ ├── SettingViewModel.kt
│ │ │ ├── TodoDetailViewModel.kt
│ │ │ ├── TodoHomeViewModel.kt
│ │ │ └── TodoListViewModel.kt
│ │ │ └── widget
│ │ │ ├── TodoListWidget.kt
│ │ │ ├── callback
│ │ │ └── TodoListWidgetCallback.kt
│ │ │ ├── dataBean
│ │ │ └── TodoListWidgetShowData.kt
│ │ │ ├── receive
│ │ │ ├── AppWidgetPinnedReceiver.kt
│ │ │ └── TodoListWidgetReceiver.kt
│ │ │ └── ui
│ │ │ └── TodoListWidgetContent.kt
│ └── res
│ │ ├── drawable
│ │ ├── bg.webp
│ │ ├── bg2.webp
│ │ ├── filter_alt_off.xml
│ │ ├── ic_checked_circle.xml
│ │ ├── ic_circle.xml
│ │ ├── ic_launcher_foreground.xml
│ │ ├── ic_unknown_user.xml
│ │ └── todo_list_widget_preview.png
│ │ ├── layout
│ │ └── widget_loading.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── raw
│ │ ├── loading.json
│ │ └── no_result.json
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ └── todo_list_widget_info.xml
│ └── test
│ └── java
│ └── com
│ └── equationl
│ └── giteetodo
│ └── ExampleUnitTest.kt
├── build.gradle
├── docs
├── description.md
└── img
│ ├── Screenshot_1.png
│ ├── Screenshot_2.png
│ ├── Screenshot_3.png
│ ├── Screenshot_4.png
│ ├── Screenshot_5.png
│ ├── Screenshot_6.png
│ ├── Screenshot_7.png
│ ├── Screenshot_8.png
│ ├── framework.png
│ ├── get_user_api.jpg
│ ├── pager.png
│ └── pkg.png
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | /app/src/main/java/com/equationl/giteetodo/test.kt
17 | /app/release/
18 | /.idea/dbnavigator.xml
19 | /.idea/kotlinc.xml
20 | /.idea/migrations.xml
21 | /.idea/deploymentTargetSelector.xml
22 | /.idea/other.xml
23 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
18 |
19 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GiteeTodo - 码云待办
2 |
3 | ## 简介
4 | GiteeTodo 是一款基于 compose + viewmodel + Retrofit 实现的 MVI 架构 TODO 应用;使用 Gitee(码云)的 issue 作为储存仓库。
5 |
6 | ### 主要功能
7 | - 查看仓库列表(仅获取类型为 *个人* ,且登录账号为 *创建者* 的仓库)
8 | - 新建仓库
9 | - 根据仓库查看 ISSUE 列表(支持筛选)
10 | - 新建 ISSUE
11 | - 快速标记 ISSUE 完成状态
12 | - 查看某个 ISSUE 详情
13 | - 编辑某个 ISSUE 详情
14 | - 支持标签(label)管理
15 | - 支持查看、新建、编辑、删除 ISSUE 评论
16 | - 适配深色模式
17 | - 支持桌面小部件
18 |
19 | ### 截图
20 | |  |  |
21 | |-----------------------------------|-----------------------------------|
22 | |  |  |
23 | |  |  |
24 | |  |  |
25 |
26 | ### 项目结构图
27 | 
28 |
29 | 
30 |
31 | ### 使用的技术栈及第三方库
32 | *基本架构: [MVI](https://juejin.cn/post/7048980213811642382)*
33 |
34 | - viewmodel: ViewModel 类旨在以注重生命周期的方式存储和管理界面相关的数据。
35 | - Hilt:Hilt 是一个适用于 Android 的依赖注入库,可减少在项目中进行手动依赖注入的样板代码。
36 | - Navigation-Animation: 为 Navigation 提供动画支持的库。
37 | - paging: 使用 Paging 库,您可以更轻松地在应用的 RecyclerView 中逐步妥善地加载数据。
38 | - swiperefresh:一个提供实现滑动刷新布局的库,类似于 Android 的 SwipeRefreshLayout。
39 | - room:Room 持久性库在 SQLite 的基础上提供了一个抽象层,让用户能够在充分利用 SQLite 的强大功能的同时,获享更强健的数据库访问机制。
40 | - placeholder:在加载内容时显示“占位符”的库。
41 | - systemuicontroller:提供易于使用的实用程序,用于更新 Jetpack Compose 中的 System UI 栏颜色。
42 | - retrofit2:网络请求库。
43 | - datastore: 以异步、一致的事务方式存储数据,克服了 SharedPreferences 的一些缺点。
44 | - coil:由 Kotlin Coroutines 支持的 Android 图像加载库。
45 | - Pager:一个用于在 Jetpack Compose 中构建分页布局的库,类似于 ViewPager。
46 | - compose-richtext: 用于在 compose 中显示富文本(包括 markdown)。
47 | - lottie: 在 Android 、 iOS、 Web 和 React Native 上原生渲染 After Effects 动画。
48 | - compose-material-dialogs: Jetpack Compose 的MD对话框。
49 |
50 | ### 下载
51 | APK: *待补充*
52 |
53 | 国内源码镜像:[https://gitee.com/equation/GiteeTodo](https://gitee.com/equation/GiteeTodo)
54 |
55 | ## 使用方法
56 |
57 | ### 编译运行
58 | 首先你需要有自己的 client_id 和 client_secret。
59 |
60 | 请前往Gitee [注册](https://gitee.com/oauth/applications/new) 获取(详情请查阅 [创建应用流程](https://gitee.com/api/v5/oauth_doc#list-item-3))。
61 |
62 | 注册信息请根据自己情况随意填写,但是**应用回调地址必须填写** `giteetodoapp://authed` 否则将无法登录。
63 |
64 | 然后在项目根目录下的 `local.properties` 文件中写入你创建的 id 和 密钥:
65 | ```
66 | CLIENT_ID = "xxxxxx"
67 | CLIENT_SECRET = "xxxxxx"
68 | ```
69 |
70 | 最后按照正常安卓程序的编译流程编译即可。
71 |
72 | ### 常规用法
73 | 由于本程序基于 Gitee 的 issue 系统,所以推荐的正确姿势是使用本应用来管理你的 Gitee 仓库 issue,方便随时新建、查看、修改 issue。
74 |
75 | ### 其他用法
76 | 还有一个比较 *“异端”* 的用法是将本程序作为一个纯粹的 TODO 程序,新建或使用你已有的 Gitee 仓库作为储存库,然后将你的 TODO 事项写入其中,一个完善的 TODO 应用应有的功能特性,本程序都有。
77 |
78 | ## 如何登录
79 | 目前不提供离线使用,使用时必须联网,且为了保证正常使用,需要授权登录码云账户。
80 |
81 | ### 登录方式
82 | 本程序支持以下三种登录方式:
83 |
84 | 1. 账号密码登录
85 | 2. 私人令牌登录
86 | 3. OAuth2 授权登录
87 |
88 | 我们推荐使用第 2 或 第 3 种方式登录,不建议使用第 1 种方式登录。
89 |
90 | #### 账号密码登录
91 | 直接使用码云账号密码登录,我们不会储存你的账号密码,该账号密码仅用于当前获取 token 这一过程,使用完毕会立即销毁。
92 |
93 | **我们不推荐使用该登录方式**
94 |
95 | #### 私人令牌登录
96 |
97 | 登录码云(必须是桌面版)后,依次点击 右上角头像 - 设置 - 安全设置 - 私人令牌 - 生成新令牌 - 勾选所需要的权限(`user_info projects issues notes`) - 提交即可。
98 |
99 | *使用私人令牌的优势: 不会暴露账号密码、权限可以自己控制、随时可以修改或删除授权。*
100 |
101 | #### OAuth2 授权登录
102 |
103 | 使用码云官方 OAuth2 认证授权。
104 |
105 | *使用 OAuth2 的优势:不会暴露账号密码、权限可以自己控制、随时可以修改或删除授权、token有效期只有一天。*
106 |
107 | **注意**:无论使用什么方式登录,我们都会储存你的授权 Token , 便于下次直接登录。
108 |
109 | 该 Token 你随时可以在 Gitee(码云)个人设置中查看并取消授权或删除、修改。
110 |
111 | 本程序绝对不会滥用用户授权的权限,仅使用本程序提到的功能,也不会读取或修改用户的其他任何信息。
112 |
113 | 如果不放心,欢迎查看源码或自行使用源码编译使用。
114 |
115 | ## 关于
116 | 由于我是第一次接触 MVI 架构,所以我也不确定这个程序是否符合 MVI 规范,如有错误,还望海涵并希望大佬们能不吝赐教。
117 |
118 | 我也是第一次接触 compose ,如有错误,还望大佬们能够指出。
119 |
120 | 该程序使用 API 来自于 Gitee 的 [OpenAPI](https://gitee.com/api/v5/swagger) 。
121 |
122 | **请勿滥用 Gitee 资源。**
123 |
124 | ### 实现思路和代码详解
125 |
126 | [使用Compose实现基于MVI架构、retrofit2、数据分页以及分页缓存且支持glance小部件的TODO应用](./docs/description.md)
127 |
128 | ### 参考资料
129 | 该程序参照了 [shenzhen2017](https://github.com/shenzhen2017) 的 [wanandroid-compose](https://github.com/shenzhen2017/wanandroid-compose)
130 |
131 | **MVI**
132 |
133 | 1. [Jetpack Compose 架构如何选? MVP, MVVM, MVI](https://juejin.cn/post/6969382803112722446)
134 | 2. [MVVM 进阶版:MVI 架构了解一下~](https://juejin.cn/post/7022624191723601928)
135 | 3. [Google 推荐使用 MVI 架构?卷起来了~](https://juejin.cn/post/7048980213811642382)
136 | 4. [MVI架构模式?到底是谁在卷?《官方架构指南升级》](https://juejin.cn/post/7058903426893086734)
137 | 5. [一文了解MVI架构,学起来吧~](https://huanglinqing.blog.csdn.net/article/details/124273344)
138 |
139 | **依赖注入**
140 |
141 | 1. [依赖注入库Hilt的使用和理解,一篇就够了](https://juejin.cn/post/6992500050790187021)
142 | 2. [Java:控制反转(IoC)与依赖注入(DI)](https://mp.weixin.qq.com/s/JaIJwuXnV0YCb1PwdXCMXg)
143 | 3. [Dependency injection with Hilt](https://developer.android.com/training/dependency-injection/hilt-android)
144 | 4. [Dagger-Hilt + Retrofit + Coroutines](https://rahul9650ray.medium.com/dagger-hilt-retrofit-coroutines-9e8af89500ab)
145 |
146 | **数据分页**
147 |
148 | 1. [Large data-sets (paging)](https://developer.android.com/jetpack/compose/lists#large-datasets)
149 | 2. [Load and display paged data ](https://developer.android.com/topic/libraries/architecture/paging/v3-paged-data)
150 | 3. [Page from network and database](https://developer.android.com/topic/libraries/architecture/paging/v3-network-db)
151 | 4. [PagingWithNetworkSample](https://github.com/android/architecture-components-samples/tree/main/PagingWithNetworkSample)
152 | 5. [Android Paging 3 library with page and limit parameters](https://www.bornfight.com/blog/android-paging-3-library-with-page-and-limit-parameters/)
153 |
154 | **glance**
155 |
156 | 1. [Jetpack Glance?小部件的春天来了](https://juejin.cn/post/7042468014251311112)
157 | 2. [Build Android App Widgets Using Jetpack Glance](https://betterprogramming.pub/android-jetpack-glance-for-app-widgets-bd7a704624ba)
158 | 3. [Announcing Jetpack Glance Alpha for app widgets](https://android-developers.googleblog.com/2021/12/announcing-jetpack-glance-alpha-for-app.html)
159 |
160 | ### 联系我
161 | email: admin@likehide.com
162 |
163 | website: [www.likehide.com](http://www.likehide.com)
164 |
165 | 其他APP:[app.likehide.com](http://app.likehide.com)
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'com.google.dagger.hilt.android'
4 | id 'org.jetbrains.kotlin.android'
5 | id 'com.google.devtools.ksp'
6 | id 'org.jetbrains.kotlin.plugin.compose'
7 | }
8 |
9 | android {
10 | compileSdk 34
11 |
12 | defaultConfig {
13 | applicationId "com.equationl.giteetodo"
14 | minSdk 24
15 | targetSdk 34
16 | versionCode 1
17 | versionName "1.0"
18 |
19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
20 | vectorDrawables {
21 | useSupportLibrary true
22 | }
23 |
24 | Properties properties = new Properties()
25 | properties.load(project.rootProject.file('local.properties').newDataInputStream())
26 | buildConfigField "String", "CLIENT_ID", properties.getProperty("CLIENT_ID")
27 | buildConfigField "String", "CLIENT_SECRET", properties.getProperty("CLIENT_SECRET")
28 | }
29 |
30 | buildTypes {
31 | release {
32 | minifyEnabled true
33 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
34 | signingConfig signingConfigs.debug
35 | }
36 | }
37 | compileOptions {
38 | // Flag to enable support for the new language APIs
39 | coreLibraryDesugaringEnabled true
40 |
41 | sourceCompatibility JavaVersion.VERSION_17
42 | targetCompatibility JavaVersion.VERSION_17
43 | }
44 | kotlinOptions {
45 | jvmTarget = '17'
46 | }
47 | buildFeatures {
48 | compose true
49 | }
50 | // composeOptions {
51 | // kotlinCompilerExtensionVersion "1.5.14"
52 | // }
53 | packagingOptions {
54 | resources {
55 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
56 | }
57 | }
58 | buildFeatures {
59 | buildConfig = true
60 | }
61 | namespace 'com.equationl.giteetodo'
62 | }
63 |
64 | // 用于忽略使用 OptIn 注解的警告
65 | //tasks.withType(KotlinCompile).configureEach {
66 | // kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
67 | //}
68 |
69 | dependencies {
70 | implementation 'androidx.core:core-ktx:1.13.1'
71 | implementation platform('androidx.compose:compose-bom:2024.06.00')
72 | implementation "androidx.compose.ui:ui"
73 | implementation "androidx.compose.material3:material3:1.3.0-beta03"
74 | implementation "androidx.compose.ui:ui-tooling"
75 | implementation "androidx.compose.material:material-icons-extended"
76 | implementation "androidx.compose.animation:animation:1.7.0-beta03"
77 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.2'
78 | implementation 'androidx.activity:activity-compose:1.9.0'
79 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0"
80 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.2'
81 | implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.2'
82 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.2'
83 | implementation "androidx.navigation:navigation-compose:2.7.7"
84 | implementation "androidx.datastore:datastore-preferences:1.1.1"
85 | implementation "androidx.datastore:datastore-core:1.1.1"
86 | implementation "androidx.paging:paging-runtime-ktx:3.3.0"
87 | implementation "androidx.paging:paging-compose:3.3.0"
88 | implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"
89 | implementation "androidx.room:room-runtime:$room_version"
90 | implementation "androidx.room:room-ktx:$room_version"
91 | ksp "androidx.room:room-compiler:$room_version"
92 | implementation "androidx.glance:glance-appwidget:1.1.0"
93 | implementation "androidx.room:room-paging:2.6.1"
94 | implementation "com.google.dagger:hilt-android:2.51.1"
95 | ksp "com.google.dagger:hilt-compiler:2.51.1"
96 | ksp "com.google.dagger:dagger-compiler:2.51.1"
97 | implementation "androidx.hilt:hilt-navigation-compose:1.2.0"
98 |
99 | implementation "io.coil-kt:coil-compose:2.6.0"
100 | implementation "io.coil-kt:coil-compose:2.6.0"
101 | implementation "io.coil-kt:coil-svg:2.6.0"
102 | implementation "io.coil-kt:coil-gif:2.6.0"
103 |
104 | // implementation "com.halilibo.compose-richtext:richtext-commonmark:0.20.0"
105 | // implementation "com.halilibo.compose-richtext:richtext-ui-material3:0.20.0"
106 | implementation "org.commonmark:commonmark:0.21.0"
107 | implementation "org.commonmark:commonmark-ext-gfm-tables:0.21.0"
108 | implementation "org.commonmark:commonmark-ext-gfm-strikethrough:0.21.0"
109 | implementation "org.commonmark:commonmark-ext-autolink:0.21.0"
110 | implementation fileTree(dir: 'libs', include: ["richtext-commonmark-release.aar", "richtext-ui-material3-release.aar", "richtext-ui-release.aar"])
111 |
112 | implementation 'com.squareup.retrofit2:retrofit:2.9.0'
113 | implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
114 | implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3'
115 | implementation "com.airbnb.android:lottie-compose:6.3.0"
116 | implementation "io.github.vanpra.compose-material-dialogs:color:0.9.0"
117 |
118 | // see: https://developer.android.com/studio/write/java8-support#library-desugaring
119 | coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
120 |
121 | testImplementation 'junit:junit:4.13.2'
122 | androidTestImplementation 'androidx.test.ext:junit:1.2.0'
123 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.0'
124 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.6.8"
125 | debugImplementation "androidx.compose.ui:ui-tooling"
126 | }
--------------------------------------------------------------------------------
/app/libs/richtext-commonmark-release.aar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/equationl/GiteeTodo/000caedd627f72c7298ad47c69571a99cdd29977/app/libs/richtext-commonmark-release.aar
--------------------------------------------------------------------------------
/app/libs/richtext-ui-material3-release.aar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/equationl/GiteeTodo/000caedd627f72c7298ad47c69571a99cdd29977/app/libs/richtext-ui-material3-release.aar
--------------------------------------------------------------------------------
/app/libs/richtext-ui-release.aar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/equationl/GiteeTodo/000caedd627f72c7298ad47c69571a99cdd29977/app/libs/richtext-ui-release.aar
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | # Please add these rules to your existing keep rules in order to suppress warnings.
24 | # This is generated automatically by the Android Gradle plugin.
25 | -dontwarn org.bouncycastle.jsse.BCSSLParameters
26 | -dontwarn org.bouncycastle.jsse.BCSSLSocket
27 | -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
28 | -dontwarn org.conscrypt.Conscrypt$Version
29 | -dontwarn org.conscrypt.Conscrypt
30 | -dontwarn org.conscrypt.ConscryptHostnameVerifier
31 | -dontwarn org.openjsse.javax.net.ssl.SSLParameters
32 | -dontwarn org.openjsse.javax.net.ssl.SSLSocket
33 | -dontwarn org.openjsse.net.ssl.OpenJSSE
34 |
35 | # keep data bean
36 | -keep class com.equationl.giteetodo.data.auth.model.** { *; }
37 | -keep class com.equationl.giteetodo.data.repos.model.** { *; }
38 | -keep class com.equationl.giteetodo.data.user.model.** { *; }
39 | -keep class com.equationl.giteetodo.widget.dataBean.** { *;}
40 |
41 | # retrofit2
42 | -keepattributes Signature, InnerClasses, EnclosingMethod
43 | -keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
44 | -keepattributes AnnotationDefault
45 | -keepclassmembers,allowshrinking,allowobfuscation interface * {
46 | @retrofit2.http.* ;
47 | }
48 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
49 | -dontwarn javax.annotation.**
50 | -dontwarn kotlin.Unit
51 | -dontwarn retrofit2.KotlinExtensions
52 | -dontwarn retrofit2.KotlinExtensions$*
53 | -if interface * { @retrofit2.http.* ; }
54 | -keep,allowobfuscation interface <1>
55 | -if interface * { @retrofit2.http.* ; }
56 | -keep,allowobfuscation interface * extends <1>
57 | -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
58 | -if interface * { @retrofit2.http.* public *** *(...); }
59 | -keep,allowoptimization,allowshrinking,allowobfuscation class <3>
60 | -keep,allowobfuscation,allowshrinking class retrofit2.Response
61 |
62 | # gson
63 | -keepattributes Signature
64 | -keepattributes RuntimeVisibleAnnotations,AnnotationDefault
65 | -if class com.google.gson.reflect.TypeToken
66 | -keep,allowobfuscation class com.google.gson.reflect.TypeToken
67 | -keep,allowobfuscation class * extends com.google.gson.reflect.TypeToken
68 | -keep,allowobfuscation,allowoptimization @com.google.gson.annotations.JsonAdapter class *
69 | -keepclassmembers,allowobfuscation class * {
70 | @com.google.gson.annotations.Expose ;
71 | @com.google.gson.annotations.JsonAdapter ;
72 | @com.google.gson.annotations.Since ;
73 | @com.google.gson.annotations.Until ;
74 | }
75 | -keepclassmembers class * extends com.google.gson.TypeAdapter {
76 | ();
77 | }
78 | -keepclassmembers class * implements com.google.gson.TypeAdapterFactory {
79 | ();
80 | }
81 | -keepclassmembers class * implements com.google.gson.JsonSerializer {
82 | ();
83 | }
84 | -keepclassmembers class * implements com.google.gson.JsonDeserializer {
85 | ();
86 | }
87 | -if class *
88 | -keepclasseswithmembers,allowobfuscation class <1> {
89 | @com.google.gson.annotations.SerializedName ;
90 | }
91 | -if class * {
92 | @com.google.gson.annotations.SerializedName ;
93 | }
94 | -keepclassmembers,allowobfuscation,allowoptimization class <1> {
95 | ();
96 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/equationl/giteetodo/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.equationl.giteetodo
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.equationl.giteetodo", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
30 |
31 |
32 |
33 |
34 |
35 |
38 |
39 |
40 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/equationl/GiteeTodo/000caedd627f72c7298ad47c69571a99cdd29977/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/equationl/giteetodo/App.kt:
--------------------------------------------------------------------------------
1 | package com.equationl.giteetodo
2 |
3 | import android.app.Application
4 | import com.equationl.giteetodo.util.datastore.DataStoreUtils
5 | import dagger.hilt.android.HiltAndroidApp
6 |
7 | @HiltAndroidApp
8 | class App: Application() {
9 | override fun onCreate() {
10 | super.onCreate()
11 | DataStoreUtils.init(this)
12 | }
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/equationl/giteetodo/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.equationl.giteetodo
2 |
3 | import android.os.Bundle
4 | import android.util.Log
5 | import androidx.activity.ComponentActivity
6 | import androidx.activity.compose.setContent
7 | import androidx.compose.animation.AnimatedContent
8 | import androidx.compose.animation.ExperimentalSharedTransitionApi
9 | import androidx.compose.animation.SharedTransitionLayout
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.Surface
13 | import androidx.compose.runtime.CompositionLocalProvider
14 | import androidx.compose.ui.Modifier
15 | import androidx.core.view.WindowCompat
16 | import androidx.navigation.compose.rememberNavController
17 | import com.equationl.giteetodo.constants.IntentDataKey
18 | import com.equationl.giteetodo.ui.HomeNavHost
19 | import com.equationl.giteetodo.ui.LocalNavController
20 | import com.equationl.giteetodo.ui.LocalShareAnimatedContentScope
21 | import com.equationl.giteetodo.ui.LocalSharedTransitionScope
22 | import com.equationl.giteetodo.ui.page.TodoDetailScreen
23 | import com.equationl.giteetodo.ui.theme.GiteeTodoTheme
24 | import dagger.hilt.android.AndroidEntryPoint
25 |
26 | @AndroidEntryPoint
27 | class MainActivity : ComponentActivity() {
28 | @OptIn(ExperimentalSharedTransitionApi::class)
29 | override fun onCreate(savedInstanceState: Bundle?) {
30 | super.onCreate(savedInstanceState)
31 |
32 | WindowCompat.setDecorFitsSystemWindows(window, false)
33 |
34 | setContent {
35 | GiteeTodoTheme {
36 | Surface(
37 | modifier = Modifier.fillMaxSize(),
38 | color = MaterialTheme.colorScheme.background
39 | ) {
40 | val issueNumber = intent.getStringExtra(IntentDataKey.ISSUE_NUMBER)
41 | val repoPath = intent.getStringExtra(IntentDataKey.REPO_PATH)
42 |
43 | Log.d("el, MainActivity", "onCreate: repoPath = $repoPath")
44 |
45 | if (issueNumber.isNullOrBlank()) {
46 | HomeNavHost()
47 | }
48 | else {
49 | SharedTransitionLayout {
50 | AnimatedContent(targetState = true, label = "testAnimatedContent") { show ->
51 | // 这里是从小组件打开的 issue 详情,逻辑上并不会存在共享元素转换效果,但是为了保证数据不为空
52 | // “强行” 造出了需要的参数,实际上这些参数并不会被使用
53 | println("show = $show")
54 | CompositionLocalProvider(
55 | LocalNavController provides rememberNavController(),
56 | LocalSharedTransitionScope provides this@SharedTransitionLayout,
57 | LocalShareAnimatedContentScope provides this@AnimatedContent,
58 | ) {
59 | TodoDetailScreen(null, issueNumber, "Loading...", issueRepo = repoPath)
60 | }
61 | }
62 | }
63 | }
64 | }
65 | }
66 | }
67 |
68 | }
69 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/equationl/giteetodo/constants/ChooseRepoType.kt:
--------------------------------------------------------------------------------
1 | package com.equationl.giteetodo.constants
2 |
3 | object ChooseRepoType {
4 | const val WIDGET_SETTING = "widgetSetting"
5 |
6 | /**
7 | * 当前设置的小组件 ID,用于设置小组件的仓库
8 | *
9 | * 这里属于是偷懒了,直接缓存到了 Object 中
10 | * */
11 | var currentWidgetAppId = -1
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/equationl/giteetodo/constants/ClientInfo.kt:
--------------------------------------------------------------------------------
1 | package com.equationl.giteetodo.constants
2 |
3 | import com.equationl.giteetodo.BuildConfig
4 |
5 | object ClientInfo {
6 | const val CLIENT_ID = BuildConfig.CLIENT_ID
7 | const val CLIENT_SECRET = BuildConfig.CLIENT_SECRET
8 |
9 | const val PERMISSION_SCOPE = "projects user_info issues notes"
10 | const val AUTH_URI = "giteetodoapp://authed"
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/equationl/giteetodo/constants/DefaultText.kt:
--------------------------------------------------------------------------------
1 | package com.equationl.giteetodo.constants
2 |
3 | object DefaultText {
4 | const val README_FILE_NAME = "README.md"
5 | const val README_CONTENT = """
6 |
7 | ***
8 |
9 | 使用 [GiteeTodo](https://github.com/equationl/GiteeTodo) 创建。
10 |
11 | > GiteeTodo 是一款基于 compose + viewmodel + Retrofit 实现的 MVI 架构 TODO 应用;使用 Gitee(码云)的 issue 作为储存仓库。
12 |
13 | """
14 |
15 |
16 | const val ABOUT_CONTENT = """
17 | ## 关于
18 | GiteeTodo 是一款基于 compose + viewmodel + Retrofit 实现的 MVI 架构 TODO 应用;使用 Gitee(码云)的 issue 作为储存仓库。
19 |
20 | ### 主要功能
21 | - 查看仓库列表(仅获取类型为 *个人* ,且登录账号为 *创建者* 的仓库)
22 | - 新建仓库
23 | - 根据仓库查看 ISSUE 列表(支持筛选)
24 | - 新建 ISSUE
25 | - 快速标记 ISSUE 完成状态
26 | - 查看某个 ISSUE 详情
27 | - 编辑某个 ISSUE 详情
28 | - 支持标签(label)管理
29 | - 支持查看、新建、编辑、删除 ISSUE 评论
30 | - 适配深色模式
31 |
32 | ### 源码地址
33 | [GiteeTodo](https://github.com/equationl/GiteeTodo)
34 |
35 | ### 联系我
36 | email: admin@likehide.com
37 |
38 | website: [www.likehide.com](http://www.likehide.com)
39 |
40 | 其他APP:[app.likehide.com](http://app.likehide.com)
41 |
42 | """
43 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/equationl/giteetodo/constants/IntentDataKey.kt:
--------------------------------------------------------------------------------
1 | package com.equationl.giteetodo.constants
2 |
3 | object IntentDataKey {
4 | const val ISSUE_NUMBER = "issueNumber"
5 | const val REPO_PATH = "repoPath"
6 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/equationl/giteetodo/constants/Net.kt:
--------------------------------------------------------------------------------
1 | package com.equationl.giteetodo.constants
2 |
3 | object Net {
4 | const val BASE_URL = "https://gitee.com/api/v5/"
5 | const val OAUTH_URL = "https://gitee.com/oauth/"
6 |
7 | const val CONNECTION_TIME_OUT = 10L
8 | const val READ_TIME_OUT = 10L
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/equationl/giteetodo/constants/ShareElementKey.kt:
--------------------------------------------------------------------------------
1 | package com.equationl.giteetodo.constants
2 |
3 | object ShareElementKey {
4 | const val TODO_ITEM_TITLE = "TODO_ITEM_TITLE"
5 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/equationl/giteetodo/data/DataBaseModule.kt:
--------------------------------------------------------------------------------
1 | package com.equationl.giteetodo.data
2 |
3 | import android.content.Context
4 | import com.equationl.giteetodo.data.repos.db.IssueDb
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.android.qualifiers.ApplicationContext
9 | import dagger.hilt.components.SingletonComponent
10 | import javax.inject.Singleton
11 |
12 | //这里使用了SingletonComponent,因此 NetworkModule 绑定到 Application 的整个生命周期
13 | @Module
14 | @InstallIn(SingletonComponent::class)
15 | object DataBaseModule {
16 |
17 | @Singleton
18 | @Provides
19 | fun provideIssueDataBase(@ApplicationContext app: Context) = run {
20 | IssueDb.create(app)
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/equationl/giteetodo/data/NetWorkMoudle.kt:
--------------------------------------------------------------------------------
1 | package com.equationl.giteetodo.data
2 |
3 | import com.equationl.giteetodo.BuildConfig
4 | import com.equationl.giteetodo.constants.Net
5 | import com.equationl.giteetodo.data.auth.OAuthApi
6 | import com.equationl.giteetodo.data.repos.RepoApi
7 | import com.equationl.giteetodo.data.user.UserApi
8 | import dagger.Module
9 | import dagger.Provides
10 | import dagger.hilt.InstallIn
11 | import dagger.hilt.components.SingletonComponent
12 | import okhttp3.OkHttpClient
13 | import okhttp3.logging.HttpLoggingInterceptor
14 | import retrofit2.Retrofit
15 | import retrofit2.converter.gson.GsonConverterFactory
16 | import java.net.Proxy
17 | import java.util.concurrent.TimeUnit
18 | import javax.inject.Singleton
19 |
20 | //这里使用了SingletonComponent,因此 NetworkModule 绑定到 Application 的整个生命周期
21 | @Module
22 | @InstallIn(SingletonComponent::class)
23 | object NetworkModule {
24 |
25 | @Singleton
26 | @Provides
27 | fun provideOkHttpClient() = run {
28 | val logging = HttpLoggingInterceptor()
29 | logging.level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.BASIC
30 | OkHttpClient.Builder()
31 | .addInterceptor(logging)
32 | .connectTimeout(Net.CONNECTION_TIME_OUT, TimeUnit.SECONDS)
33 | .readTimeout(Net.READ_TIME_OUT, TimeUnit.SECONDS)
34 | .proxy(Proxy.NO_PROXY)
35 | .build()
36 | }
37 |
38 | @Singleton
39 | @Provides
40 | fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit = Retrofit.Builder()
41 | .addConverterFactory(GsonConverterFactory.create())
42 | .baseUrl(Net.BASE_URL)
43 | .client(okHttpClient)
44 | .build()
45 |
46 | @Singleton
47 | @Provides
48 | fun provideRepoApiService(retrofit: Retrofit): RepoApi = retrofit.create(RepoApi::class.java)
49 |
50 | @Singleton
51 | @Provides
52 | fun provideUserApiService(retrofit: Retrofit): UserApi = retrofit.create(UserApi::class.java)
53 |
54 | @Singleton
55 | @Provides
56 | fun provideOAuthApiService(okHttpClient: OkHttpClient): OAuthApi =
57 | // 因为 OAuthA 使用的URL不一样,所以重新创建一个
58 | Retrofit.Builder()
59 | .addConverterFactory(GsonConverterFactory.create())
60 | .baseUrl(Net.OAUTH_URL)
61 | .client(okHttpClient)
62 | .build()
63 | .create(OAuthApi::class.java)
64 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/equationl/giteetodo/data/auth/OAuthApi.kt:
--------------------------------------------------------------------------------
1 | package com.equationl.giteetodo.data.auth
2 |
3 | import com.equationl.giteetodo.data.auth.model.response.Token
4 | import retrofit2.Response
5 | import retrofit2.http.Field
6 | import retrofit2.http.FormUrlEncoded
7 | import retrofit2.http.POST
8 | import retrofit2.http.Query
9 |
10 | interface OAuthApi {
11 | @FormUrlEncoded
12 | @POST("token")
13 | suspend fun getTokenByPsw(
14 | @Field("username") userName: String,
15 | @Field("password") password: String,
16 | @Field("client_id") clientId: String,
17 | @Field("client_secret") clientSecret: String,
18 | @Field("scope") scope: String = "projects user_info issues notes",
19 | @Field("grant_type") grantType: String = "password"
20 | ): Response
21 |
22 | @POST("token")
23 | suspend fun getTokenByCode(
24 | @Query("code") code: String,
25 | @Query("client_id") clientId: String,
26 | @Query("redirect_uri") redirectUri: String,
27 | @Query("client_secret") clientSecret: String,
28 | @Query("grant_type") grantType: String = "authorization_code"
29 | ): Response
30 |
31 | @POST("token")
32 | suspend fun refreshToken(
33 | @Query("refresh_token") refreshToken: String,
34 | @Query("grant_type") grantType: String = "refresh_token"
35 | ): Response
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/equationl/giteetodo/data/auth/model/response/Token.kt:
--------------------------------------------------------------------------------
1 | package com.equationl.giteetodo.data.auth.model.response
2 | import com.google.gson.annotations.SerializedName
3 |
4 |
5 | data class Token(
6 | @SerializedName("access_token")
7 | val accessToken: String,
8 | @SerializedName("created_at")
9 | val createdAt: Int,
10 | @SerializedName("expires_in")
11 | val expiresIn: Int,
12 | @SerializedName("refresh_token")
13 | val refreshToken: String,
14 | @SerializedName("scope")
15 | val scope: String,
16 | @SerializedName("token_type")
17 | val tokenType: String
18 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/equationl/giteetodo/data/repos/db/IssueConverters.kt:
--------------------------------------------------------------------------------
1 | package com.equationl.giteetodo.data.repos.db
2 |
3 | import androidx.room.TypeConverter
4 | import com.equationl.giteetodo.data.repos.model.response.Label
5 | import com.equationl.giteetodo.ui.common.IssueState
6 | import com.google.gson.Gson
7 | import com.google.gson.reflect.TypeToken
8 |
9 | class IssueConverters {
10 |
11 | @TypeConverter
12 | fun fromLabelList(value: List