├── .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 | 41 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | 16 | 17 | 18 | 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 | | ![1](./docs/img/Screenshot_1.png) | ![2](./docs/img/Screenshot_2.png) | 21 | |-----------------------------------|-----------------------------------| 22 | | ![3](./docs/img/Screenshot_3.png) | ![4](./docs/img/Screenshot_4.png) | 23 | | ![5](./docs/img/Screenshot_5.png) | ![6](./docs/img/Screenshot_6.png) | 24 | | ![7](./docs/img/Screenshot_7.png) | ![8](./docs/img/Screenshot_8.png) | 25 | 26 | ### 项目结构图 27 | ![包结构](./docs/img/pkg.png) 28 | 29 | ![架构](./docs/img/framework.png) 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