diff --git a/Demo/app/build.gradle b/Demo/app/build.gradle index c88fc9a..4cb665e 100644 --- a/Demo/app/build.gradle +++ b/Demo/app/build.gradle @@ -3,6 +3,7 @@ plugins { id 'org.jetbrains.kotlin.android' id 'kotlin-kapt' id 'androidx.navigation.safeargs.kotlin' + id 'com.google.dagger.hilt.android' } android { @@ -37,11 +38,17 @@ android { viewBinding true dataBinding true } + + kapt { + correctErrorTypes true + } } dependencies { def lifecycle_version = "2.6.1" def nav_version = "2.5.3" + def retrofit_version = "2.9.0" + def hilt_version = "2.46.1" implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.appcompat:appcompat:1.6.1' @@ -54,6 +61,7 @@ dependencies { implementation "androidx.preference:preference-ktx:1.2.0" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1" implementation 'androidx.fragment:fragment-ktx:1.5.7' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1" // Navigation implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" @@ -65,6 +73,17 @@ dependencies { // OkHttp3 implementation "com.squareup.okhttp3:okhttp:4.10.0" + // Retrofit2 + implementation "com.squareup.retrofit2:retrofit:$retrofit_version" + implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" + + // Hilt + implementation "com.google.dagger:hilt-android:$hilt_version" + kapt "com.google.dagger:hilt-compiler:$hilt_version" + + // Glide + implementation "com.github.bumptech.glide:glide:4.13.2" + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' diff --git a/Demo/app/src/main/AndroidManifest.xml b/Demo/app/src/main/AndroidManifest.xml index 3a1bbf8..7f07300 100644 --- a/Demo/app/src/main/AndroidManifest.xml +++ b/Demo/app/src/main/AndroidManifest.xml @@ -87,10 +87,33 @@ + android:exported="true" + android:theme="@style/GitHubTheme"> + + + + + + + + + + + + + +) + +sealed interface ProfileDetail { + + val type: DetailType + + data class ProfileName( + val avatar: String, + val username: String, + val name: String? = null, + override val type: DetailType = DetailType.NAME + ) : ProfileDetail + + data class ProfileInfo( + @DrawableRes val icon: Int, + val title: String, + override val type: DetailType = DetailType.INFO + ) : ProfileDetail { + companion object { + + fun from(userResponse: UserResponse) = buildList { + with(userResponse) { + bio?.let { + add(ProfileInfo(R.drawable.ic_info, it)) + } + company?.let { + add(ProfileInfo(R.drawable.ic_chat, it)) + } + blog?.let { + add(ProfileInfo(R.drawable.ic_link_logo, it)) + } + email?.let { + add(ProfileInfo(R.drawable.ic_email, it)) + } + add( + ProfileInfo( + R.drawable.ic_person_24, + ResourceHelper.resources.getString(R.string.follow_info, followers, following) + ) + ) + } + } + } + } +} + +sealed interface ProfileInfo { + val type: ProfileType + + data class ProfileCard( + val profileDetail: List, + override val type: ProfileType = ProfileType.DETAIL + ) : ProfileInfo { + companion object { + + fun from(userResponse: UserResponse): ProfileCard { + val profileName = with(userResponse) { + ProfileDetail.ProfileName(avatarUrl, username, name) + } + val profileDetails: MutableList = + ProfileDetail.ProfileInfo.from(userResponse).toMutableList() + profileDetails.add(0, profileName) + return ProfileCard(profileDetails) + } + } + } + + data class ProfileItem( + @DrawableRes val icon: Int, + @ColorInt val iconBackground: Int, + val title: String, + val count: Int? = null, + override val type: ProfileType = ProfileType.ITEM + ) : ProfileInfo { + + companion object { + fun from(userResponse: UserResponse) = with(userResponse) { + with(ResourceHelper.resources) { + listOf( + ProfileItem( + R.drawable.ic_repo_24, + getColor(R.color.github_pull, null), + getString(R.string.repositories), + publicRepos + ), ProfileItem( + R.drawable.ic_git_pull_request, + getColor(R.color.github_gists, null), + getString(R.string.gists), + publicGists + ), ProfileItem( + R.drawable.ic_organization_24, + getColor(R.color.github_organization, null), + getString(R.string.organizations) + ), ProfileItem( + R.drawable.ic_star_24, + getColor(R.color.github_starred, null), + getString(R.string.starred) + ) + ) + } + } + } + } +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/githubclient/data/local/Release.kt b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/local/Release.kt new file mode 100644 index 0000000..5efba7d --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/local/Release.kt @@ -0,0 +1,19 @@ +package com.krunal.demo.githubclient.data.local + +import java.util.Date + +data class Release( + val repoFullName: String, + val releaseName: String, + val author: String, + val authorAvatar: String, + val releaseDate: Date, + val releaseAssets: List +) + +data class ReleaseAsset( + val name: String, + val size: Int, + val contentType: String, + val downloadUrl: String, +) \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/githubclient/data/local/Repo.kt b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/local/Repo.kt new file mode 100644 index 0000000..8a1d95c --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/local/Repo.kt @@ -0,0 +1,50 @@ +package com.krunal.demo.githubclient.data.local + +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import com.krunal.demo.R +import com.krunal.demo.githubclient.data.remote.model.response.RepositoryResponse +import com.krunal.demo.helpers.ResourceHelper + +data class RepoCard( + val avatar: String, val username: String, val repository: String, val repoFullName: String +) + +data class RepoDetail( + val avatar: String, + val username: String, + val repository: String, + val repoFullName: String, + val description: String?, + val website: String?, + val starCount: Int, + val watchCount: Int?, + val forkCount: Int?, + val repoItems: List = emptyList() +) + +data class RepoDetailItem( + @DrawableRes val icon: Int, + @ColorInt val iconBackground: Int, + val title: String, + val count: Int? = null +) { + companion object { + fun from(repositoryResponse: RepositoryResponse) = with(repositoryResponse) { + with(ResourceHelper.resources) { + listOf( + RepoDetailItem( + R.drawable.ic_git_pull_request, + getColor(R.color.github_pull, null), + getString(R.string.issues), + ), + RepoDetailItem( + R.drawable.ic_git_pull_request, + getColor(R.color.github_pull, null), + getString(R.string.releases) + ), + ) + } + } + } +} diff --git a/Demo/app/src/main/java/com/krunal/demo/githubclient/data/local/UpdateProfileDetail.kt b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/local/UpdateProfileDetail.kt new file mode 100644 index 0000000..46a1e56 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/local/UpdateProfileDetail.kt @@ -0,0 +1,82 @@ +package com.krunal.demo.githubclient.data.local + +import androidx.databinding.BaseObservable +import androidx.databinding.Bindable +import com.krunal.demo.BR +import com.krunal.demo.githubclient.data.remote.model.response.UserResponse + +data class UpdateProfileDetail( + private var _avatar: String?, + private var _name: String?, + private var _email: String, + private var _bio: String?, + private var _website: String?, + private var _twitter: String?, + private var _company: String? +) : BaseObservable() { + + @get:Bindable + var avatar: String? = _avatar + set(value) { + _avatar = value + field = value + notifyPropertyChanged(BR.avatar) + } + + @get:Bindable + var name: String? = _name + set(value) { + _name = value + field = value + notifyPropertyChanged(BR.name) + } + + @get:Bindable + var email: String = _email + set(value) { + _email = value + field = value + notifyPropertyChanged(BR.email) + } + + @get:Bindable + var bio: String? = _bio + set(value) { + _bio = value + field = value + notifyPropertyChanged(BR.bio) + } + + @get:Bindable + var website: String? = _website + set(value) { + _website = value + field = value + notifyPropertyChanged(BR.website) + } + + @get:Bindable + var twitter: String? = _twitter + set(value) { + _twitter = value + field = value + notifyPropertyChanged(BR.twitter) + } + + @get:Bindable + var company: String? = _company + set(value) { + _company = value + field = value + notifyPropertyChanged(BR.company) + } + + companion object { + + fun from(userResponse: UserResponse): UpdateProfileDetail = with(userResponse) { + UpdateProfileDetail( + avatarUrl, name, email, bio, blog, twitterUsername, company + ) + } + } +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/api/AuthorizationService.kt b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/api/AuthorizationService.kt new file mode 100644 index 0000000..5cbfa87 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/api/AuthorizationService.kt @@ -0,0 +1,15 @@ +package com.krunal.demo.githubclient.data.remote.api + +import com.krunal.demo.githubclient.data.remote.model.request.AuthorizationRequest +import com.krunal.demo.githubclient.data.remote.model.response.AuthorizationResponse +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.Headers +import retrofit2.http.POST + +interface AuthorizationService { + + @Headers("Accept: application/json") + @POST("access_token") + suspend fun getAuthorizationToken(@Body body: AuthorizationRequest): Response +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/api/FileService.kt b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/api/FileService.kt new file mode 100644 index 0000000..93eb319 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/api/FileService.kt @@ -0,0 +1,28 @@ +package com.krunal.demo.githubclient.data.remote.api + +import com.krunal.demo.githubclient.data.remote.model.response.ImageUploadResponse +import okhttp3.MultipartBody +import okhttp3.ResponseBody +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part +import retrofit2.http.Query +import retrofit2.http.Streaming +import retrofit2.http.Url + +interface FileService { + + @Streaming + @GET + suspend fun downloadFile(@Url url: String): ResponseBody + + @Multipart + @POST("") + suspend fun uploadImageFile( + @Url url: String, + @Query("key") key: String, + @Part image: MultipartBody.Part + ): Response +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/api/IssueService.kt b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/api/IssueService.kt new file mode 100644 index 0000000..09c24db --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/api/IssueService.kt @@ -0,0 +1,22 @@ +package com.krunal.demo.githubclient.data.remote.api + +import com.krunal.demo.githubclient.data.remote.model.request.IssueRequest +import com.krunal.demo.githubclient.data.remote.model.response.IssueResponse +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.PATCH +import retrofit2.http.POST +import retrofit2.http.Path + +interface IssueService { + + @POST("repos/{repo}/issues") + suspend fun createIssue(@Path("repo", encoded = true) repo: String, @Body issue: IssueRequest): Response + + @PATCH("repos/{repo}/issues") + suspend fun updateIssue(@Path("repo", encoded = true) repo: String, @Body issue: IssueRequest): Response + + @GET("repos/{repo}/issues/{issueId}") + suspend fun getIssue(@Path("repo", encoded = true) repo: String, @Path("issueId") issueId: Int): Response +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/api/NotificationService.kt b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/api/NotificationService.kt new file mode 100644 index 0000000..2b774ac --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/api/NotificationService.kt @@ -0,0 +1,13 @@ +package com.krunal.demo.githubclient.data.remote.api + +import com.krunal.demo.githubclient.data.remote.model.response.NotificationsResponseItem +import com.krunal.demo.githubclient.data.remote.model.response.UserResponse +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Query + +interface NotificationService { + + @GET("notifications") + suspend fun getNotifications(@Query("all") all: Boolean = true, @Query("per_page") pageSize: Int = 10): Response> +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/api/RepoService.kt b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/api/RepoService.kt new file mode 100644 index 0000000..745e9c9 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/api/RepoService.kt @@ -0,0 +1,27 @@ +package com.krunal.demo.githubclient.data.remote.api + +import com.krunal.demo.githubclient.data.local.Release +import com.krunal.demo.githubclient.data.remote.model.response.ReleaseResponse +import com.krunal.demo.githubclient.data.remote.model.response.RepositoryResponse +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.PUT +import retrofit2.http.Path + +interface RepoService { + + @GET("user/repos") + suspend fun getAuthorizedUserRepos(): Response> + + @GET("users/{username}/repos") + suspend fun getRepos(@Path("username") username: String): Response> + + @PUT("repos/{repo}/contents/{path}") + suspend fun createFile(@Path("repo", encoded = true) repo: String, @Path("path") filePath: String): Response> + + @GET("repos/{repoName}") + suspend fun getRepo(@Path("repoName", encoded = true) repoName: String): Response + + @GET("repos/{repoName}/releases/latest") + suspend fun getLatestRelease(@Path("repoName", encoded = true) repoName: String): Response +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/api/UserService.kt b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/api/UserService.kt new file mode 100644 index 0000000..0741544 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/api/UserService.kt @@ -0,0 +1,28 @@ +package com.krunal.demo.githubclient.data.remote.api + +import com.krunal.demo.githubclient.data.remote.model.request.LogoutRequest +import com.krunal.demo.githubclient.data.remote.model.request.UpdateProfileRequest +import com.krunal.demo.githubclient.data.remote.model.response.UserResponse +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.HTTP +import retrofit2.http.PATCH +import retrofit2.http.Path + +interface UserService { + + @GET("user") + suspend fun getAuthorizedUser(): Response + + @PATCH("user") + suspend fun updateUser(@Body updateProfileRequest: UpdateProfileRequest): Response + + @HTTP( + method = "DELETE", + path = "applications/{clientId}/token", + hasBody = true + ) + suspend fun logout(@Path("clientId") clientId: String, @Body body: LogoutRequest): Response +} \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/request/AuthorizationRequest.kt b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/request/AuthorizationRequest.kt new file mode 100644 index 0000000..567f8bc --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/request/AuthorizationRequest.kt @@ -0,0 +1,9 @@ +package com.krunal.demo.githubclient.data.remote.model.request + +data class AuthorizationRequest( + val clientId: String, + val clientSecret: String, + val code: String, + val redirectUri: String? = null, + val repositoryId: String? = null +) \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/request/IssueRequest.kt b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/request/IssueRequest.kt new file mode 100644 index 0000000..0568b86 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/request/IssueRequest.kt @@ -0,0 +1,9 @@ +package com.krunal.demo.githubclient.data.remote.model.request + +data class IssueRequest( + val title: String, + val body: String? = null, + val milestone: String? = null, + val labels: List = emptyList(), + val assignees: List = emptyList() +) \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/request/LogoutRequest.kt b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/request/LogoutRequest.kt new file mode 100644 index 0000000..1a331fc --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/request/LogoutRequest.kt @@ -0,0 +1,5 @@ +package com.krunal.demo.githubclient.data.remote.model.request + +data class LogoutRequest( + val accessToken: String +) \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/request/UpdateProfileRequest.kt b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/request/UpdateProfileRequest.kt new file mode 100644 index 0000000..877589d --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/request/UpdateProfileRequest.kt @@ -0,0 +1,12 @@ +package com.krunal.demo.githubclient.data.remote.model.request + +data class UpdateProfileRequest( + val name: String?, + val email: String?, + val blog: String?, + val twitterUsername: String?, + val company: String?, + val location: String?, + val hireable: Boolean?, + val bio: String? +) \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/response/AuthorizationResponse.kt b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/response/AuthorizationResponse.kt new file mode 100644 index 0000000..2278161 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/response/AuthorizationResponse.kt @@ -0,0 +1,10 @@ +package com.krunal.demo.githubclient.data.remote.model.response + +data class AuthorizationResponse( + val accessToken: String, + val expiresIn: Int, + val refreshToken: String, + val refreshTokenExpiresIn: Int, + val scope: String, + val tokenType: String +) \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/response/ErrorResponse.kt b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/response/ErrorResponse.kt new file mode 100644 index 0000000..7ea1d45 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/response/ErrorResponse.kt @@ -0,0 +1,12 @@ +package com.krunal.demo.githubclient.data.remote.model.response + +data class AuthorizationErrorResponse( + val error: String, + val errorDescription: String, + val errorUri: String +) + +data class ApiError( + val documentationUrl: String, + val message: String +) \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/response/ImageUploadResponse.kt b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/response/ImageUploadResponse.kt new file mode 100644 index 0000000..0250afa --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/response/ImageUploadResponse.kt @@ -0,0 +1,34 @@ +package com.krunal.demo.githubclient.data.remote.model.response + +data class ImageUploadResponse( + val `data`: Data, val status: Int, val success: Boolean +) + +data class Medium( + val extension: String, val filename: String, val mime: String, val name: String, val url: String +) + +data class Image( + val extension: String, val filename: String, val mime: String, val name: String, val url: String +) + +data class Data( + val deleteUrl: String, + val displayUrl: String, + val expiration: String, + val height: String, + val id: String, + val image: Image, + val medium: Medium, + val size: String, + val thumb: Thumb, + val time: String, + val title: String, + val url: String, + val urlViewer: String, + val width: String +) + +data class Thumb( + val extension: String, val filename: String, val mime: String, val name: String, val url: String +) \ No newline at end of file diff --git a/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/response/IssueResponse.kt b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/response/IssueResponse.kt new file mode 100644 index 0000000..94e25f5 --- /dev/null +++ b/Demo/app/src/main/java/com/krunal/demo/githubclient/data/remote/model/response/IssueResponse.kt @@ -0,0 +1,60 @@ +package com.krunal.demo.githubclient.data.remote.model.response + +data class IssueResponse( + val activeLockReason: String, + val assignee: UserResponse, + val assignees: List, + val authorAssociation: String, + val body: String, + val closedAt: Any?, + val closedBy: UserResponse, + val comments: Int, + val commentsUrl: String, + val createdAt: String, + val eventsUrl: String, + val htmlUrl: String, + val id: Int, + val labels: List