├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── themes.xml
│ │ │ ├── drawable
│ │ │ │ ├── ic_anime.jpg
│ │ │ │ ├── ic_arts.jpg
│ │ │ │ ├── ic_cars.jpg
│ │ │ │ ├── ic_city.jpg
│ │ │ │ ├── ic_dark.jpg
│ │ │ │ ├── ic_food.jpg
│ │ │ │ ├── ic_love.jpg
│ │ │ │ ├── ic_macro.jpg
│ │ │ │ ├── ic_music.jpg
│ │ │ │ ├── ic_space.jpg
│ │ │ │ ├── ic_tech.jpg
│ │ │ │ ├── ic_words.jpg
│ │ │ │ ├── ic_animals.jpg
│ │ │ │ ├── ic_flowers.jpg
│ │ │ │ ├── ic_nature.jpg
│ │ │ │ ├── ic_splash.jpg
│ │ │ │ ├── ic_sports.jpg
│ │ │ │ ├── ic_twitter.png
│ │ │ │ ├── ic_vector.jpg
│ │ │ │ ├── ic_abstract.jpg
│ │ │ │ ├── ic_holidays.jpg
│ │ │ │ ├── ic_instagram.png
│ │ │ │ ├── ic_unsplash.png
│ │ │ │ ├── ic_motorcycles.jpg
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ ├── ic_launcher_foreground.xml
│ │ │ │ └── ic_empty_bookmark.xml
│ │ │ ├── font
│ │ │ │ ├── robot_light.ttf
│ │ │ │ ├── roboto_bold.ttf
│ │ │ │ ├── roboto_thin.ttf
│ │ │ │ ├── roboto_medium.ttf
│ │ │ │ └── roboto_regular.ttf
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ └── values-night
│ │ │ │ └── themes.xml
│ │ ├── ic_background-playstore.png
│ │ ├── ic_foreground-playstore.png
│ │ ├── ic_launcher_background-playstore.png
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── eneskayiklik
│ │ │ │ └── wallup
│ │ │ │ ├── feature_detail
│ │ │ │ ├── domain
│ │ │ │ │ ├── model
│ │ │ │ │ │ ├── DownloadType.kt
│ │ │ │ │ │ ├── ScreenType.kt
│ │ │ │ │ │ ├── DetailScreenNavArgs.kt
│ │ │ │ │ │ ├── DetailState.kt
│ │ │ │ │ │ └── DetailEvent.kt
│ │ │ │ │ ├── repository
│ │ │ │ │ │ └── DetailRepository.kt
│ │ │ │ │ └── use_case
│ │ │ │ │ │ └── DetailUseCase.kt
│ │ │ │ ├── data
│ │ │ │ │ └── repository
│ │ │ │ │ │ └── DetailRepositoryImpl.kt
│ │ │ │ └── presentation
│ │ │ │ │ ├── component
│ │ │ │ │ ├── DetailImageContent.kt
│ │ │ │ │ ├── DetailImageItem.kt
│ │ │ │ │ ├── DetailRelatedCollection.kt
│ │ │ │ │ ├── LoadingAnim.kt
│ │ │ │ │ ├── DetailButtonStack.kt
│ │ │ │ │ └── DetailImageInfoItem.kt
│ │ │ │ │ ├── DetailScreen.kt
│ │ │ │ │ └── DetailViewModel.kt
│ │ │ │ ├── WallUpApp.kt
│ │ │ │ ├── feature_home
│ │ │ │ ├── domain
│ │ │ │ │ ├── model
│ │ │ │ │ │ ├── ColorItem.kt
│ │ │ │ │ │ ├── Category.kt
│ │ │ │ │ │ ├── HomeState.kt
│ │ │ │ │ │ ├── HomeEvent.kt
│ │ │ │ │ │ └── UnsplashPhoto.kt
│ │ │ │ │ ├── repository
│ │ │ │ │ │ └── HomeRepository.kt
│ │ │ │ │ └── use_case
│ │ │ │ │ │ └── HomeUseCase.kt
│ │ │ │ ├── presentation
│ │ │ │ │ ├── component
│ │ │ │ │ │ ├── SectionTitle.kt
│ │ │ │ │ │ ├── WelcomeSection.kt
│ │ │ │ │ │ ├── ColorSection.kt
│ │ │ │ │ │ ├── CategoriesSection.kt
│ │ │ │ │ │ └── SuggestedSection.kt
│ │ │ │ │ ├── HomeViewModel.kt
│ │ │ │ │ └── HomeScreen.kt
│ │ │ │ └── data
│ │ │ │ │ ├── dto
│ │ │ │ │ └── UnsplashPhotoDto.kt
│ │ │ │ │ └── repository
│ │ │ │ │ └── HomeRepositoryImpl.kt
│ │ │ │ ├── utils
│ │ │ │ ├── const
│ │ │ │ │ └── Constants.kt
│ │ │ │ ├── model
│ │ │ │ │ └── UiEvent.kt
│ │ │ │ ├── network
│ │ │ │ │ ├── Resource.kt
│ │ │ │ │ └── HttpRoutes.kt
│ │ │ │ ├── extensions
│ │ │ │ │ ├── Context.kt
│ │ │ │ │ ├── Int.kt
│ │ │ │ │ └── LazyListScope.kt
│ │ │ │ ├── broadcast_receiver
│ │ │ │ │ ├── BroadcastReceiver.kt
│ │ │ │ │ └── ShakeManager.kt
│ │ │ │ ├── transfer_extensions
│ │ │ │ │ └── UnsplashPhoto.kt
│ │ │ │ └── blur_hash
│ │ │ │ │ └── BlurHashDecoder.kt
│ │ │ │ ├── feature_collection
│ │ │ │ ├── domain
│ │ │ │ │ ├── model
│ │ │ │ │ │ ├── CollectionScreenNavArgs.kt
│ │ │ │ │ │ └── CollectionState.kt
│ │ │ │ │ ├── repository
│ │ │ │ │ │ ├── SearchRepository.kt
│ │ │ │ │ │ └── CollectionRepository.kt
│ │ │ │ │ └── use_case
│ │ │ │ │ │ ├── SearchUseCase.kt
│ │ │ │ │ │ └── CollectionUseCase.kt
│ │ │ │ ├── data
│ │ │ │ │ ├── dto
│ │ │ │ │ │ └── SearchResponseDto.kt
│ │ │ │ │ └── repository
│ │ │ │ │ │ ├── CollectionRepositoryImpl.kt
│ │ │ │ │ │ └── SearchRepositoryImpl.kt
│ │ │ │ └── presentation
│ │ │ │ │ ├── component
│ │ │ │ │ ├── TitleSection.kt
│ │ │ │ │ └── ItemsSection.kt
│ │ │ │ │ ├── CollectionScreen.kt
│ │ │ │ │ └── CollectionViewModel.kt
│ │ │ │ ├── feature_bookmark
│ │ │ │ ├── domain
│ │ │ │ │ ├── repository
│ │ │ │ │ │ └── BookmarkRepository.kt
│ │ │ │ │ ├── model
│ │ │ │ │ │ └── BookmarkState.kt
│ │ │ │ │ └── use_case
│ │ │ │ │ │ └── BookmarkUseCase.kt
│ │ │ │ ├── data
│ │ │ │ │ ├── db
│ │ │ │ │ │ ├── BookmarkDatabase.kt
│ │ │ │ │ │ ├── entity
│ │ │ │ │ │ │ └── BookmarkPhoto.kt
│ │ │ │ │ │ └── dao
│ │ │ │ │ │ │ └── BookmarkPhotoDao.kt
│ │ │ │ │ └── repository
│ │ │ │ │ │ └── BookmarkRepositoryImpl.kt
│ │ │ │ └── presentation
│ │ │ │ │ ├── BookmarkViewModel.kt
│ │ │ │ │ ├── component
│ │ │ │ │ ├── EmptyBookmarkSection.kt
│ │ │ │ │ └── BookmarkSection.kt
│ │ │ │ │ └── BookmarkScreen.kt
│ │ │ │ ├── ui
│ │ │ │ ├── theme
│ │ │ │ │ ├── Shape.kt
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Theme.kt
│ │ │ │ │ └── Type.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ └── animation
│ │ │ │ │ └── ScreensAnim.kt
│ │ │ │ ├── di
│ │ │ │ ├── DetailModule.kt
│ │ │ │ └── HomeModule.kt
│ │ │ │ └── feature_splash
│ │ │ │ └── presentation
│ │ │ │ └── SplashScreen.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── eneskayiklik
│ │ │ └── wallup
│ │ │ ├── ExampleUnitTest.kt
│ │ │ └── utils
│ │ │ └── extensions
│ │ │ └── IntExtensionTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── eneskayiklik
│ │ └── wallup
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── .idea
├── .gitignore
├── compiler.xml
├── vcs.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
└── misc.xml
├── screenshots
├── cover_photo.png
├── home_screen.jpg
├── detail_screen.jpg
├── bookmark_screen.jpg
├── collection_screen.jpg
└── architecture_diagram.png
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── settings.gradle
├── .gitignore
├── gradle.properties
├── gradlew.bat
├── README.md
├── gradlew
└── LICENSE
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | WallUp
3 |
--------------------------------------------------------------------------------
/screenshots/cover_photo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/screenshots/cover_photo.png
--------------------------------------------------------------------------------
/screenshots/home_screen.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/screenshots/home_screen.jpg
--------------------------------------------------------------------------------
/screenshots/detail_screen.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/screenshots/detail_screen.jpg
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/screenshots/bookmark_screen.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/screenshots/bookmark_screen.jpg
--------------------------------------------------------------------------------
/screenshots/collection_screen.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/screenshots/collection_screen.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_anime.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_anime.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_arts.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_arts.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_cars.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_cars.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_city.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_city.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_dark.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_food.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_food.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_love.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_love.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_macro.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_macro.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_music.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_music.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_space.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_space.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_tech.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_tech.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_words.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_words.jpg
--------------------------------------------------------------------------------
/app/src/main/res/font/robot_light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/font/robot_light.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/font/roboto_bold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/font/roboto_thin.ttf
--------------------------------------------------------------------------------
/screenshots/architecture_diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/screenshots/architecture_diagram.png
--------------------------------------------------------------------------------
/app/src/main/ic_background-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/ic_background-playstore.png
--------------------------------------------------------------------------------
/app/src/main/ic_foreground-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/ic_foreground-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_animals.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_animals.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_flowers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_flowers.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_nature.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_nature.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_splash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_splash.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_sports.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_sports.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_twitter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_twitter.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_vector.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_vector.jpg
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/font/roboto_medium.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/roboto_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/font/roboto_regular.ttf
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_abstract.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_abstract.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_holidays.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_holidays.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_instagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_instagram.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_unsplash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_unsplash.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_motorcycles.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/res/drawable/ic_motorcycles.jpg
--------------------------------------------------------------------------------
/app/src/main/ic_launcher_background-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Enes-Kayiklik/Wall-Up/HEAD/app/src/main/ic_launcher_background-playstore.png
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_detail/domain/model/DownloadType.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_detail.domain.model
2 |
3 | enum class DownloadType {
4 | SHARE,
5 | NONE
6 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_detail/domain/model/ScreenType.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_detail.domain.model
2 |
3 | enum class ScreenType {
4 | HOME,
5 | BOTH,
6 | LOCK
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/WallUpApp.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class WallUpApp: Application()
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_home/domain/model/ColorItem.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_home.domain.model
2 |
3 | data class ColorItem(
4 | val hexCode: String,
5 | val name: String = "",
6 | )
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_detail/domain/model/DetailScreenNavArgs.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_detail.domain.model
2 |
3 | data class DetailScreenNavArgs(
4 | val id: String,
5 | val thumbnail: String?
6 | )
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | rootProject.name = "WallUp"
9 | include ':app'
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/utils/const/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.utils.const
2 |
3 | const val UNSPLASH_URL = "https://unsplash.com/"
4 |
5 | const val TWITTER_LINK = "https://twitter.com/"
6 | const val INSTAGRAM_LINK = "https://www.instagram.com/"
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_home/domain/model/Category.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_home.domain.model
2 |
3 | import androidx.annotation.DrawableRes
4 |
5 | data class Category(
6 | val title: String = "",
7 | @DrawableRes val imageRes: Int = -1,
8 | )
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_collection/domain/model/CollectionScreenNavArgs.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_collection.domain.model
2 |
3 | data class CollectionScreenNavArgs(
4 | val title: String?,
5 | val searchQuery: String?,
6 | val collectionId: String?
7 | )
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_home/domain/model/HomeState.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_home.domain.model
2 |
3 | data class HomeState(
4 | val colorList: List = emptyList(),
5 | val categories: List = emptyList(),
6 | val randomPhotos: List? = null
7 | )
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_home/domain/model/HomeEvent.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_home.domain.model
2 |
3 | import com.ramcosta.composedestinations.spec.Direction
4 |
5 | sealed class HomeEvent {
6 | data class Navigate(val route: Direction) : HomeEvent()
7 | object ScrollTop : HomeEvent()
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_bookmark/domain/repository/BookmarkRepository.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_bookmark.domain.repository
2 |
3 | import com.eneskayiklik.wallup.feature_bookmark.data.db.entity.BookmarkPhoto
4 |
5 | interface BookmarkRepository {
6 |
7 | suspend fun getAllBookmarks(): List
8 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | *.apk
3 | *.aar
4 | *.ap_
5 | *.aab
6 | .gradle
7 | /local.properties
8 | /.idea/caches
9 | /.idea/libraries
10 | /.idea/modules.xml
11 | /.idea/workspace.xml
12 | /.idea/navEditor.xml
13 | /.idea/assetWizardSettings.xml
14 | .DS_Store
15 | /build
16 | /captures
17 | .externalNativeBuild
18 | .cxx
19 | local.properties
20 | key_store
21 | *.keystore
22 | *.pepk
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_collection/domain/model/CollectionState.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_collection.domain.model
2 |
3 | import com.eneskayiklik.wallup.feature_home.domain.model.UnsplashPhoto
4 |
5 | data class CollectionState(
6 | val title: String = "",
7 | val count: Int = 0,
8 | val items: List = emptyList()
9 | )
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_bookmark/domain/model/BookmarkState.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_bookmark.domain.model
2 |
3 | import com.eneskayiklik.wallup.feature_bookmark.data.db.entity.BookmarkPhoto
4 |
5 | data class BookmarkState(
6 | val title: String = "Bookmarked Items",
7 | val count: Int = 0,
8 | val items: List = emptyList()
9 | )
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/utils/model/UiEvent.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.utils.model
2 |
3 | import com.ramcosta.composedestinations.spec.Direction
4 |
5 | sealed class UiEvent {
6 | data class OnNavigate(val route: Direction) : UiEvent()
7 | object PopBack : UiEvent()
8 | object ScrollTop : UiEvent()
9 | data class ShowToast(val title: String) : UiEvent()
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_collection/domain/repository/SearchRepository.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_collection.domain.repository
2 |
3 | import com.eneskayiklik.wallup.feature_home.data.dto.UnsplashPhotoDto
4 | import com.eneskayiklik.wallup.utils.network.Resource
5 |
6 | interface SearchRepository {
7 |
8 | suspend fun getSearchData(query: String): Resource>
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_collection/data/dto/SearchResponseDto.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_collection.data.dto
2 |
3 | import com.eneskayiklik.wallup.feature_home.data.dto.UnsplashPhotoDto
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class SearchResponseDto(
8 | val results: List,
9 | val total: Int,
10 | val total_pages: Int
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_collection/domain/repository/CollectionRepository.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_collection.domain.repository
2 |
3 | import com.eneskayiklik.wallup.feature_home.data.dto.UnsplashPhotoDto
4 | import com.eneskayiklik.wallup.utils.network.Resource
5 |
6 | interface CollectionRepository {
7 |
8 | suspend fun getCollectionData(collectionId: String): Resource>
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/utils/network/Resource.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.utils.network
2 |
3 | sealed class Resource(
4 | val response: T? = null,
5 | val errorMessage: String? = null
6 | ) {
7 | data class Success(val data: T) : Resource(data)
8 | data class Error(val message: String, val data: T? = null) : Resource(data, message)
9 | class Loading : Resource()
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/test/java/com/eneskayiklik/wallup/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_detail/domain/model/DetailState.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_detail.domain.model
2 |
3 | import android.graphics.drawable.Drawable
4 | import com.eneskayiklik.wallup.feature_home.domain.model.UnsplashPhoto
5 |
6 | data class DetailState(
7 | val thumbnail: String? = null,
8 | val imageDrawable: Drawable? = null,
9 | val currentDownloadId: Long? = null,
10 | val imageDetail: UnsplashPhoto? = null
11 | )
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val LightOnBackground = Color(0xFF1E3054)
6 | val LightBackground = Color(0xFFF4F7FD)
7 | val LightSurface = Color(0xFFFFFFFF)
8 | val DarkOnBackground = Color(0xFFF5CAC9)
9 | val DarkBackground = Color(0xFF0C1B3A)
10 | val DarkSurface = Color(0xFF162544)
11 | val LightPrimary = Color(0xFF1E3054)
12 | val DarkPrimary = Color(0xFFF5CAC9)
13 | val PrimaryVariant = Color(0xFF8A7D8F)
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_bookmark/data/db/BookmarkDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_bookmark.data.db
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import com.eneskayiklik.wallup.feature_bookmark.data.db.entity.BookmarkPhoto
6 | import com.eneskayiklik.wallup.feature_bookmark.data.db.dao.BookmarkPhotoDao
7 |
8 | @Database(entities = [BookmarkPhoto::class], version = 1)
9 | abstract class BookmarkDatabase: RoomDatabase() {
10 | abstract fun bookmarkDao(): BookmarkPhotoDao
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_bookmark/domain/use_case/BookmarkUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_bookmark.domain.use_case
2 |
3 | import com.eneskayiklik.wallup.feature_bookmark.domain.repository.BookmarkRepository
4 | import com.eneskayiklik.wallup.utils.network.Resource
5 | import kotlinx.coroutines.flow.flow
6 | import javax.inject.Inject
7 |
8 | class BookmarkUseCase @Inject constructor(
9 | private val repository: BookmarkRepository
10 | ) {
11 | suspend fun getAllBookmarks() = repository.getAllBookmarks()
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_bookmark/data/db/entity/BookmarkPhoto.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_bookmark.data.db.entity
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 |
7 | @Entity(tableName = "bookmark")
8 | data class BookmarkPhoto(
9 | @PrimaryKey(autoGenerate = true) val id: Int = 0,
10 | @ColumnInfo(name = "unsplash_id") val unsplashId: String,
11 | @ColumnInfo(name = "thumbnail") val thumbnail: String,
12 | @ColumnInfo(name = "hex_color") val color: String,
13 | )
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/utils/network/HttpRoutes.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.utils.network
2 |
3 | object HttpRoutes {
4 | private const val BASE_URL = "https://api.unsplash.com"
5 | const val RANDOM_PHOTO = "$BASE_URL/photos/random"
6 | const val PHOTO = "$BASE_URL/photos"
7 | const val COLLECTION = "$BASE_URL/collections/{id}/photos"
8 | const val SEARCH = "$BASE_URL/search/photos"
9 | }
10 |
11 | object HttpParam {
12 | const val CLIENT_ID = "client_id"
13 | const val COUNT = "count"
14 | const val PER_PAGE = "per_page"
15 | const val QUERY = "query"
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/utils/extensions/Context.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.utils.extensions
2 |
3 | import android.content.Context
4 | import android.graphics.Bitmap
5 | import android.net.Uri
6 | import android.provider.MediaStore
7 | import java.io.ByteArrayOutputStream
8 |
9 | fun Context.getImageUri(bitmap: Bitmap?): Uri {
10 | val bytes = ByteArrayOutputStream()
11 | bitmap?.compress(Bitmap.CompressFormat.JPEG, 100, bytes)
12 | val path = MediaStore.Images.Media.insertImage(
13 | contentResolver, bitmap, System.currentTimeMillis().toString(), null
14 | )
15 | return Uri.parse(path)
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_home/domain/repository/HomeRepository.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_home.domain.repository
2 |
3 | import com.eneskayiklik.wallup.feature_home.data.dto.UnsplashPhotoDto
4 | import com.eneskayiklik.wallup.feature_home.domain.model.Category
5 | import com.eneskayiklik.wallup.feature_home.domain.model.ColorItem
6 | import com.eneskayiklik.wallup.utils.network.Resource
7 |
8 | interface HomeRepository {
9 |
10 | suspend fun getColorList(): List
11 |
12 | suspend fun getCategoryList(): List
13 |
14 | suspend fun getRandomPhotos(): Resource>
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_detail/domain/repository/DetailRepository.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_detail.domain.repository
2 |
3 | import com.eneskayiklik.wallup.feature_bookmark.data.db.entity.BookmarkPhoto
4 | import com.eneskayiklik.wallup.feature_home.data.dto.UnsplashPhotoDto
5 | import com.eneskayiklik.wallup.utils.network.Resource
6 |
7 | interface DetailRepository {
8 |
9 | suspend fun getImageDetail(id: String): Resource
10 |
11 | suspend fun isBookmarked(id: String): Boolean
12 |
13 | suspend fun addBookmark(item: BookmarkPhoto)
14 |
15 | suspend fun removeBookmark(id: String)
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_bookmark/data/repository/BookmarkRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_bookmark.data.repository
2 |
3 | import com.eneskayiklik.wallup.feature_bookmark.domain.repository.BookmarkRepository
4 | import com.eneskayiklik.wallup.feature_bookmark.data.db.entity.BookmarkPhoto
5 | import com.eneskayiklik.wallup.feature_bookmark.data.db.dao.BookmarkPhotoDao
6 | import javax.inject.Inject
7 |
8 | class BookmarkRepositoryImpl @Inject constructor(
9 | private val dao: BookmarkPhotoDao
10 | ): BookmarkRepository {
11 |
12 | override suspend fun getAllBookmarks(): List {
13 | return dao.getAllBookmarks()
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_home/presentation/component/SectionTitle.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_home.presentation.component
2 |
3 | import androidx.compose.material.MaterialTheme
4 | import androidx.compose.material.Text
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.graphics.Color
8 | import androidx.compose.ui.text.font.FontWeight
9 | import androidx.compose.ui.unit.sp
10 |
11 | @Composable
12 | fun SectionTitle(
13 | modifier: Modifier = Modifier,
14 | title: String
15 | ) {
16 | Text(
17 | text = title,
18 | style = MaterialTheme.typography.h6,
19 | modifier = modifier
20 | )
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_bookmark/data/db/dao/BookmarkPhotoDao.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_bookmark.data.db.dao
2 |
3 | import androidx.room.*
4 | import com.eneskayiklik.wallup.feature_bookmark.data.db.entity.BookmarkPhoto
5 |
6 | @Dao
7 | interface BookmarkPhotoDao {
8 |
9 | @Query("SELECT * FROM bookmark WHERE unsplash_id = :id")
10 | fun getSingleBookmark(id: String): BookmarkPhoto?
11 |
12 | @Query("SELECT * FROM bookmark")
13 | fun getAllBookmarks(): List
14 |
15 | @Insert(onConflict = OnConflictStrategy.REPLACE)
16 | fun addBookmark(data: BookmarkPhoto)
17 |
18 | @Query("DELETE FROM bookmark WHERE unsplash_id = :id")
19 | fun removeBookmark(id: String)
20 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/eneskayiklik/wallup/utils/extensions/IntExtensionTest.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.utils.extensions
2 |
3 | import com.google.common.truth.Truth.assertThat
4 | import org.junit.Test
5 | import org.junit.runner.RunWith
6 | import org.junit.runners.JUnit4
7 |
8 | @RunWith(JUnit4::class)
9 | class IntExtensionTest {
10 |
11 | @Test
12 | fun `validate parse count`() {
13 | val counts = listOf(100, 18_340, 372_992, 823_498_239, 9_384, 2_048)
14 | val results = listOf("100", "18.3K", "372.9K", "823.4M", "9.3K", "2K")
15 | val result = counts.map { it.parseCount() }
16 | result.forEachIndexed { index, s ->
17 | assertThat(s).isEqualTo(results[index])
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/eneskayiklik/wallup/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup
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.naber.wallup", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/di/DetailModule.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.di
2 |
3 | import android.app.DownloadManager
4 | import android.app.WallpaperManager
5 | import android.content.Context
6 | import dagger.Module
7 | import dagger.Provides
8 | import dagger.hilt.InstallIn
9 | import dagger.hilt.android.qualifiers.ApplicationContext
10 | import dagger.hilt.components.SingletonComponent
11 | import javax.inject.Singleton
12 |
13 | @Module
14 | @InstallIn(SingletonComponent::class)
15 | object DetailModule {
16 |
17 | @Singleton
18 | @Provides
19 | fun provideDownloadManager(@ApplicationContext context: Context): DownloadManager =
20 | context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
21 |
22 | @Singleton
23 | @Provides
24 | fun provideWallpaperManager(@ApplicationContext context: Context): WallpaperManager =
25 | WallpaperManager.getInstance(context)
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/utils/extensions/Int.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.utils.extensions
2 |
3 | fun Int.parseCount(): String {
4 | return when (this) {
5 | in 1_000..999_999 -> {
6 | val result = StringBuilder("")
7 | val concat = this % 1000
8 | result.append("${this / 1_000}")
9 | if (concat >= 100)
10 | result.append(".${concat.toString().first()}")
11 | result.append("K")
12 | result.toString()
13 | }
14 | in 1_000_000..999_999_999 -> {
15 | val result = StringBuilder("")
16 | val concat = this % 1_000_000
17 | result.append("${this / 1_000_000}")
18 | if (concat >= 100)
19 | result.append(".${concat.toString().first()}")
20 | result.append("M")
21 | result.toString()
22 | }
23 | else -> "$this"
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_detail/domain/model/DetailEvent.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_detail.domain.model
2 |
3 | import android.content.Context
4 | import android.graphics.drawable.Drawable
5 |
6 | sealed class DetailEvent {
7 | data class OnBookmarkClick(
8 | val id: String,
9 | val thumbnail: String,
10 | val color: String,
11 | val addBookmark: Boolean
12 | ) : DetailEvent()
13 |
14 | data class OnDownloadClick(val url: String, val createdAt: String) : DetailEvent()
15 | data class OnWallpaper(
16 | val context: Context,
17 | val bitmap: Drawable?
18 | ) : DetailEvent()
19 |
20 | data class Navigate(val id: String?, val title: String? = null) : DetailEvent()
21 | data class UpdateDrawable(val drawable: Drawable?) : DetailEvent()
22 | data class Share(val drawable: Drawable?, val context: Context) : DetailEvent()
23 | data class ShareText(val data: String, val context: Context) : DetailEvent()
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_collection/domain/use_case/SearchUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_collection.domain.use_case
2 |
3 | import com.eneskayiklik.wallup.feature_collection.domain.repository.SearchRepository
4 | import com.eneskayiklik.wallup.utils.network.Resource
5 | import com.eneskayiklik.wallup.utils.transfer_extensions.toUIModel
6 | import kotlinx.coroutines.flow.flow
7 | import javax.inject.Inject
8 |
9 | class SearchUseCase @Inject constructor(
10 | private val repository: SearchRepository
11 | ) {
12 | suspend fun getSearchData(query: String) = flow {
13 | when (val data = repository.getSearchData(query)) {
14 | is Resource.Error -> emit(Resource.Error(data.message))
15 | is Resource.Loading -> emit(Resource.Loading())
16 | is Resource.Success -> {
17 | try {
18 | emit(Resource.Success(data.data.map { it.toUIModel() }))
19 | } catch (e: Exception) {
20 | emit(Resource.Error(e.message ?: "An unexpected error occurred"))
21 | }
22 | }
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_collection/domain/use_case/CollectionUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_collection.domain.use_case
2 |
3 | import com.eneskayiklik.wallup.feature_collection.domain.repository.CollectionRepository
4 | import com.eneskayiklik.wallup.utils.network.Resource
5 | import com.eneskayiklik.wallup.utils.transfer_extensions.toUIModel
6 | import kotlinx.coroutines.flow.flow
7 | import javax.inject.Inject
8 |
9 | class CollectionUseCase @Inject constructor(
10 | private val repository: CollectionRepository
11 | ) {
12 | suspend fun getCollectionData(collectionId: String) = flow {
13 | when (val data = repository.getCollectionData(collectionId)) {
14 | is Resource.Error -> emit(Resource.Error(data.message))
15 | is Resource.Loading -> emit(Resource.Loading())
16 | is Resource.Success -> {
17 | try {
18 | emit(Resource.Success(data.data.map { it.toUIModel() }))
19 | } catch (e: Exception) {
20 | emit(Resource.Error(e.message ?: "An unexpected error occurred"))
21 | }
22 | }
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
14 |
19 |
22 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.darkColors
6 | import androidx.compose.material.lightColors
7 | import androidx.compose.runtime.Composable
8 |
9 | private val DarkColorPalette = darkColors(
10 | primary = LightPrimary,
11 | primaryVariant = PrimaryVariant,
12 | background = DarkBackground,
13 | onBackground = DarkOnBackground,
14 | surface = DarkSurface,
15 | onSurface = DarkOnBackground
16 | )
17 |
18 | private val LightColorPalette = lightColors(
19 | primary = DarkPrimary,
20 | primaryVariant = PrimaryVariant,
21 | background = LightBackground,
22 | onBackground = LightOnBackground,
23 | surface = LightSurface,
24 | onSurface = LightOnBackground
25 | )
26 |
27 | @Composable
28 | fun WallUpTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
29 | val colors = if (darkTheme) {
30 | DarkColorPalette
31 | } else {
32 | LightColorPalette
33 | }
34 |
35 | MaterialTheme(
36 | colors = colors,
37 | typography = Typography,
38 | shapes = Shapes,
39 | content = content
40 | )
41 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
18 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_bookmark/presentation/BookmarkViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_bookmark.presentation
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.eneskayiklik.wallup.feature_bookmark.domain.model.BookmarkState
6 | import com.eneskayiklik.wallup.feature_bookmark.domain.use_case.BookmarkUseCase
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.flow.MutableStateFlow
10 | import kotlinx.coroutines.flow.StateFlow
11 | import kotlinx.coroutines.launch
12 | import javax.inject.Inject
13 |
14 | @HiltViewModel
15 | class BookmarkViewModel @Inject constructor(
16 | private val bookmarkUseCase: BookmarkUseCase
17 | ): ViewModel() {
18 |
19 | private val _bookmarkState = MutableStateFlow(BookmarkState())
20 | val bookmarkState: StateFlow = _bookmarkState
21 |
22 | init {
23 | getBookmarks()
24 | }
25 |
26 | private fun getBookmarks() {
27 | viewModelScope.launch(Dispatchers.IO) {
28 | val items = bookmarkUseCase.getAllBookmarks()
29 | _bookmarkState.value = _bookmarkState.value.copy(
30 | count = items.count(),
31 | items = items
32 | )
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_collection/data/repository/CollectionRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_collection.data.repository
2 |
3 | import com.eneskayiklik.wallup.BuildConfig
4 | import com.eneskayiklik.wallup.feature_collection.domain.repository.CollectionRepository
5 | import com.eneskayiklik.wallup.feature_home.data.dto.UnsplashPhotoDto
6 | import com.eneskayiklik.wallup.utils.network.HttpParam
7 | import com.eneskayiklik.wallup.utils.network.HttpRoutes
8 | import com.eneskayiklik.wallup.utils.network.Resource
9 | import io.ktor.client.*
10 | import io.ktor.client.request.*
11 | import javax.inject.Inject
12 |
13 | class CollectionRepositoryImpl @Inject constructor(
14 | private val client: HttpClient
15 | ): CollectionRepository {
16 |
17 | override suspend fun getCollectionData(collectionId: String): Resource> {
18 | return try {
19 | val data: List = client.get {
20 | url(HttpRoutes.COLLECTION.replace("{id}", collectionId))
21 | parameter(HttpParam.CLIENT_ID, BuildConfig.API_KEY)
22 | parameter(HttpParam.PER_PAGE, Int.MAX_VALUE)
23 | }
24 | Resource.Success(data)
25 | } catch (e: Exception) {
26 | Resource.Error(e.message ?: "An error occurred")
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_home/domain/use_case/HomeUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_home.domain.use_case
2 |
3 | import com.eneskayiklik.wallup.feature_home.domain.repository.HomeRepository
4 | import com.eneskayiklik.wallup.utils.network.Resource
5 | import com.eneskayiklik.wallup.utils.transfer_extensions.toUIModel
6 | import kotlinx.coroutines.flow.flow
7 | import javax.inject.Inject
8 |
9 | class HomeUseCase @Inject constructor(
10 | private val repository: HomeRepository
11 | ) {
12 | suspend fun getRandomPhotos() = flow {
13 | emit(Resource.Loading())
14 | when (val data = repository.getRandomPhotos()) {
15 | is Resource.Error -> emit(Resource.Error(data.message))
16 | is Resource.Loading -> emit(Resource.Loading())
17 | is Resource.Success -> {
18 | try {
19 | emit(Resource.Success(data.data.map { it.toUIModel() }))
20 | } catch (e: Exception) {
21 | emit(Resource.Error("An unexpected error occurred"))
22 | }
23 | }
24 | }
25 | }
26 |
27 | suspend fun getColorList() = flow {
28 | emit(repository.getColorList().shuffled())
29 | }
30 |
31 | suspend fun getCategoryList() = flow {
32 | emit(repository.getCategoryList().shuffled())
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/utils/broadcast_receiver/BroadcastReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.utils.broadcast_receiver
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.IntentFilter
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.DisposableEffect
9 | import androidx.compose.ui.platform.LocalContext
10 |
11 | /**
12 | * Check this link:
13 | * https://developer.android.com/jetpack/compose/interop/interop-apis#case-study-broadcastreceivers
14 | * */
15 | @Composable
16 | fun SystemBroadcastReceiver(
17 | systemAction: String,
18 | onSystemEvent: (intent: Intent?) -> Unit
19 | ) {
20 | val context = LocalContext.current
21 |
22 | // If either context or systemAction changes, unregister and register again
23 | DisposableEffect(context, systemAction) {
24 | val intentFilter = IntentFilter(systemAction)
25 | val broadcast = object : BroadcastReceiver() {
26 | override fun onReceive(context: Context?, intent: Intent?) {
27 | onSystemEvent(intent)
28 | }
29 | }
30 | context.registerReceiver(broadcast, intentFilter)
31 |
32 | // When the effect leaves the Composition, remove the callback
33 | onDispose {
34 | context.unregisterReceiver(broadcast)
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_collection/data/repository/SearchRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_collection.data.repository
2 |
3 | import com.eneskayiklik.wallup.BuildConfig
4 | import com.eneskayiklik.wallup.feature_collection.data.dto.SearchResponseDto
5 | import com.eneskayiklik.wallup.feature_collection.domain.repository.SearchRepository
6 | import com.eneskayiklik.wallup.feature_home.data.dto.UnsplashPhotoDto
7 | import com.eneskayiklik.wallup.utils.network.HttpParam
8 | import com.eneskayiklik.wallup.utils.network.HttpRoutes
9 | import com.eneskayiklik.wallup.utils.network.Resource
10 | import io.ktor.client.*
11 | import io.ktor.client.request.*
12 | import javax.inject.Inject
13 |
14 | class SearchRepositoryImpl @Inject constructor(
15 | private val client: HttpClient
16 | ): SearchRepository {
17 |
18 | override suspend fun getSearchData(query: String): Resource> {
19 | return try {
20 | val data: SearchResponseDto = client.get {
21 | url(HttpRoutes.SEARCH)
22 | parameter(HttpParam.CLIENT_ID, BuildConfig.API_KEY)
23 | parameter(HttpParam.PER_PAGE, Int.MAX_VALUE)
24 | parameter(HttpParam.QUERY, query)
25 | }
26 | Resource.Success(data.results)
27 | } catch (e: Exception) {
28 | Resource.Error(e.message ?: "An error occurred")
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_bookmark/presentation/component/EmptyBookmarkSection.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_bookmark.presentation.component
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.material.MaterialTheme
6 | import androidx.compose.material.Text
7 | import androidx.compose.runtime.*
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.res.painterResource
11 | import androidx.compose.ui.unit.dp
12 | import com.eneskayiklik.wallup.R
13 |
14 | @Composable
15 | fun EmptyBookmarkSection() {
16 | Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
17 | Column(
18 | verticalArrangement = Arrangement.spacedBy(4.dp),
19 | horizontalAlignment = Alignment.CenterHorizontally
20 | ) {
21 | Image(
22 | painter = painterResource(id = R.drawable.ic_empty_bookmark),
23 | contentDescription = "Empty Bookmark"
24 | )
25 | Text(
26 | text = "Nothing!", style = MaterialTheme.typography.h1.copy(
27 | color = MaterialTheme.colors.onBackground
28 | )
29 | )
30 | Text(
31 | text = "Your bookmark list is empty.", style = MaterialTheme.typography.h6.copy(
32 | color = MaterialTheme.colors.onBackground
33 | )
34 | )
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_collection/presentation/component/TitleSection.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_collection.presentation.component
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.lazy.LazyListScope
5 | import androidx.compose.material.MaterialTheme
6 | import androidx.compose.material.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.unit.sp
10 |
11 | fun LazyListScope.titleSection(title: String, count: Int, modifier: Modifier = Modifier) {
12 | item {
13 | Column(modifier = modifier) {
14 | if (title.isNotEmpty()) {
15 | TitleSection(title = title)
16 | }
17 | if (count != 0) {
18 | CountSection(count = count)
19 | }
20 | }
21 | }
22 |
23 | }
24 |
25 | @Composable
26 | private fun TitleSection(title: String, modifier: Modifier = Modifier) {
27 | Text(
28 | modifier = modifier, text = title, style = MaterialTheme.typography.h6.copy(
29 | color = MaterialTheme.colors.onBackground,
30 | fontSize = 32.sp
31 | )
32 | )
33 | }
34 |
35 | @Composable
36 | private fun CountSection(count: Int, modifier: Modifier = Modifier) {
37 | Text(
38 | modifier = modifier,
39 | text = "$count wallpaper available",
40 | style = MaterialTheme.typography.body1.copy(
41 | color = MaterialTheme.colors.onBackground
42 | )
43 | )
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/utils/extensions/LazyListScope.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.utils.extensions
2 |
3 | import androidx.compose.foundation.ExperimentalFoundationApi
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.foundation.lazy.LazyListScope
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 |
9 | @ExperimentalFoundationApi
10 | fun LazyListScope.gridItems(
11 | data: List,
12 | columnCount: Int,
13 | modifier: Modifier,
14 | horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
15 | itemContent: @Composable BoxScope.(T) -> Unit,
16 | ) {
17 | val size = data.count()
18 | val rows = if (size == 0) 0 else 1 + (size - 1) / columnCount
19 | items(rows, key = { it.hashCode() }) { rowIndex ->
20 | Row(
21 | horizontalArrangement = horizontalArrangement,
22 | modifier = modifier.animateItemPlacement()
23 | ) {
24 | for (columnIndex in 0 until columnCount) {
25 | val itemIndex = rowIndex * columnCount + columnIndex
26 | if (itemIndex < size) {
27 | Box(
28 | modifier = Modifier.weight(1F, fill = true),
29 | propagateMinConstraints = true
30 | ) {
31 | itemContent(data[itemIndex])
32 | }
33 | } else {
34 | Spacer(Modifier.weight(1F, fill = true))
35 | }
36 | }
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_detail/domain/use_case/DetailUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_detail.domain.use_case
2 |
3 | import com.eneskayiklik.wallup.feature_bookmark.data.db.entity.BookmarkPhoto
4 | import com.eneskayiklik.wallup.feature_detail.domain.repository.DetailRepository
5 | import com.eneskayiklik.wallup.utils.network.Resource
6 | import com.eneskayiklik.wallup.utils.transfer_extensions.toUIModel
7 | import kotlinx.coroutines.flow.flow
8 | import javax.inject.Inject
9 |
10 | class DetailUseCase @Inject constructor(
11 | private val repository: DetailRepository
12 | ) {
13 |
14 | suspend fun getPhotoDetail(id: String) = flow {
15 | emit(Resource.Loading())
16 | when (val data = repository.getImageDetail(id)) {
17 | is Resource.Error -> emit(Resource.Error(data.message))
18 | is Resource.Loading -> emit(Resource.Loading())
19 | is Resource.Success -> {
20 | try {
21 | emit(Resource.Success(data.data.toUIModel().copy(isBookmarked = isBookmarked(id))))
22 | } catch (e: Exception) {
23 | emit(Resource.Error(e.message ?: "An unexpected error occurred"))
24 | }
25 | }
26 | }
27 | }
28 |
29 | suspend fun addBookmark(item: BookmarkPhoto) {
30 | repository.addBookmark(item)
31 | }
32 |
33 | suspend fun removeBookmark(id: String) {
34 | repository.removeBookmark(id)
35 | }
36 |
37 | private suspend fun isBookmarked(id: String) = repository.isBookmarked(id)
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/ui/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.ui
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.animation.ExperimentalAnimationApi
7 | import androidx.compose.foundation.ExperimentalFoundationApi
8 | import androidx.compose.material.ExperimentalMaterialApi
9 | import androidx.compose.ui.ExperimentalComposeUiApi
10 | import androidx.compose.ui.unit.ExperimentalUnitApi
11 | import coil.annotation.ExperimentalCoilApi
12 | import com.eneskayiklik.wallup.NavGraphs
13 | import com.eneskayiklik.wallup.ui.theme.WallUpTheme
14 | import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi
15 | import com.ramcosta.composedestinations.DestinationsNavHost
16 | import com.ramcosta.composedestinations.animations.rememberAnimatedNavHostEngine
17 | import dagger.hilt.android.AndroidEntryPoint
18 |
19 | @ExperimentalAnimationApi
20 | @AndroidEntryPoint
21 | @ExperimentalFoundationApi
22 | @ExperimentalCoilApi
23 | @ExperimentalMaterialApi
24 | @ExperimentalComposeUiApi
25 | @ExperimentalUnitApi
26 | @ExperimentalMaterialNavigationApi
27 | class MainActivity : ComponentActivity() {
28 | override fun onCreate(savedInstanceState: Bundle?) {
29 | super.onCreate(savedInstanceState)
30 | setContent {
31 | WallUpTheme {
32 | DestinationsNavHost(
33 | navGraph = NavGraphs.root,
34 | engine = rememberAnimatedNavHostEngine()
35 | )
36 | }
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_detail/data/repository/DetailRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_detail.data.repository
2 |
3 | import android.util.Log
4 | import com.eneskayiklik.wallup.BuildConfig
5 | import com.eneskayiklik.wallup.feature_bookmark.data.db.entity.BookmarkPhoto
6 | import com.eneskayiklik.wallup.feature_bookmark.data.db.dao.BookmarkPhotoDao
7 | import com.eneskayiklik.wallup.feature_detail.domain.repository.DetailRepository
8 | import com.eneskayiklik.wallup.feature_home.data.dto.UnsplashPhotoDto
9 | import com.eneskayiklik.wallup.utils.network.HttpParam
10 | import com.eneskayiklik.wallup.utils.network.HttpRoutes
11 | import com.eneskayiklik.wallup.utils.network.Resource
12 | import io.ktor.client.*
13 | import io.ktor.client.request.*
14 | import javax.inject.Inject
15 |
16 | class DetailRepositoryImpl @Inject constructor(
17 | private val client: HttpClient,
18 | private val bookmarkDao: BookmarkPhotoDao
19 | ) : DetailRepository {
20 |
21 | override suspend fun getImageDetail(id: String): Resource {
22 | return try {
23 | val data: UnsplashPhotoDto = client.get {
24 | url(HttpRoutes.PHOTO.plus("/$id"))
25 | parameter(HttpParam.CLIENT_ID, BuildConfig.API_KEY)
26 | }
27 | Resource.Success(data)
28 | } catch (e: Exception) {
29 | Resource.Error(e.message ?: "An unexpected error occurred")
30 | }
31 | }
32 |
33 | override suspend fun isBookmarked(id: String): Boolean {
34 | return bookmarkDao.getSingleBookmark(id) != null
35 | }
36 |
37 | override suspend fun addBookmark(item: BookmarkPhoto) = bookmarkDao.addBookmark(item)
38 |
39 | override suspend fun removeBookmark(id: String) = bookmarkDao.removeBookmark(id)
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/utils/transfer_extensions/UnsplashPhoto.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.utils.transfer_extensions
2 |
3 | import com.eneskayiklik.wallup.feature_home.data.dto.RelatedResult
4 | import com.eneskayiklik.wallup.feature_home.data.dto.UnsplashPhotoDto
5 | import com.eneskayiklik.wallup.feature_home.domain.model.*
6 |
7 | fun UnsplashPhotoDto.toUIModel() = UnsplashPhoto(
8 | id = id ?: "",
9 | createdAt = created_at ?: "",
10 | blurHash = blur_hash ?: "",
11 | color = color ?: "",
12 | thumbnail = urls?.thumb ?: "",
13 | smallImage = urls?.small ?: "",
14 | fullImage = urls?.full ?: "",
15 | sourceUrl = links?.html ?: "",
16 | views = views ?: 0,
17 | downloads = downloads ?: 0,
18 | likes = likes ?: 0,
19 | userDetail = UserDetail(
20 | name = user?.name ?: "",
21 | unsplashProfile = user?.links?.html ?: "",
22 | image = user?.profile_image?.medium ?: "",
23 | instaUserName = user?.instagram_username ?: "",
24 | twitterUserName = user?.twitter_username ?: "",
25 | ),
26 | photoDetail = PhotoDetail(
27 | width = width ?: 0,
28 | height = height ?: 0,
29 | _cameraName = exif?.name ?: "",
30 | _aperture = exif?.aperture ?: "",
31 | _focalLength = exif?.focal_length ?: "",
32 | _exposureTime = exif?.exposure_time ?: "",
33 | _iso = exif?.iso?.toString() ?: ""
34 | ),
35 | relatedCollections = RelatedCollections(
36 | collections = related_collections?.results?.map { it.toUIModel() } ?: emptyList(),
37 | total = related_collections?.total ?: 0
38 | )
39 | )
40 |
41 | fun RelatedResult.toUIModel() = RelatedCollectionResult(
42 | id = id ?: "",
43 | title = title ?: "",
44 | color = cover_photo?.color ?: "",
45 | coverPhoto = cover_photo?.urls?.regular ?: ""
46 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.ui.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.Font
6 | import androidx.compose.ui.text.font.FontFamily
7 | import androidx.compose.ui.text.font.FontWeight
8 | import androidx.compose.ui.unit.sp
9 | import com.eneskayiklik.wallup.R
10 |
11 | private val RobotoBold = FontFamily(Font(R.font.roboto_bold))
12 | private val RobotoLight = FontFamily(Font(R.font.robot_light))
13 | private val RobotoThin = FontFamily(Font(R.font.roboto_thin))
14 | private val RobotoMedium = FontFamily(Font(R.font.roboto_medium))
15 | private val RobotoRegular = FontFamily(Font(R.font.roboto_regular))
16 |
17 | // Set of Material typography styles to start with
18 | val Typography = Typography(
19 | body1 = TextStyle(
20 | fontFamily = RobotoBold,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 16.sp
23 | ),
24 | h1 = TextStyle(
25 | fontFamily = RobotoBold,
26 | fontWeight = FontWeight.Normal,
27 | fontSize = 48.sp
28 | ),
29 | h4 = TextStyle(
30 | fontFamily = RobotoBold,
31 | fontWeight = FontWeight.Normal,
32 | fontSize = 32.sp
33 | ),
34 | h6 = TextStyle(
35 | fontFamily = RobotoBold,
36 | fontWeight = FontWeight.Normal,
37 | fontSize = 18.sp
38 | ),
39 | subtitle2 = TextStyle(
40 | fontFamily = RobotoMedium,
41 | fontWeight = FontWeight.Normal,
42 | fontSize = 14.sp
43 | ),
44 | body2 = TextStyle(
45 | fontFamily = RobotoLight,
46 | fontWeight = FontWeight.Normal,
47 | fontSize = 12.sp
48 | ),
49 | caption = TextStyle(
50 | fontFamily = RobotoRegular,
51 | fontWeight = FontWeight.Normal,
52 | fontSize = 12.sp
53 | )
54 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_home/domain/model/UnsplashPhoto.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_home.domain.model
2 |
3 | data class UnsplashPhoto(
4 | val id: String,
5 | val createdAt: String,
6 | val blurHash: String,
7 | val color: String,
8 | val thumbnail: String,
9 | val smallImage: String,
10 | val fullImage: String,
11 | val views: Int,
12 | val downloads: Int,
13 | val likes: Int,
14 | val sourceUrl: String,
15 | val photoDetail: PhotoDetail,
16 | val userDetail: UserDetail,
17 | val relatedCollections: RelatedCollections,
18 | val isBookmarked: Boolean = false
19 | )
20 |
21 | data class UserDetail(
22 | val name: String,
23 | val image: String,
24 | val instaUserName: String,
25 | val twitterUserName: String,
26 | val unsplashProfile: String,
27 | )
28 |
29 | data class PhotoDetail(
30 | val width: Int,
31 | val height: Int,
32 | private val _cameraName: String,
33 | private val _aperture: String,
34 | private val _focalLength: String,
35 | private val _exposureTime: String,
36 | private val _iso: String
37 | ) {
38 | val resolution = if (width != 0 && height != 0) "${width}x$height" else "Unknown"
39 | val camera = if (_cameraName.isNotEmpty()) _cameraName else "Unknown"
40 | val aperture = if (_aperture.isNotEmpty()) _aperture else "Unknown"
41 | val focalLength = if (_focalLength.isNotEmpty()) _focalLength else "Unknown"
42 | val exposureTime = if (_exposureTime.isNotEmpty()) _exposureTime else "Unknown"
43 | val iso = if (_iso.isNotEmpty()) _iso else "Unknown"
44 | }
45 |
46 | data class RelatedCollections(
47 | val collections: List,
48 | val total: Int
49 | )
50 |
51 | data class RelatedCollectionResult(
52 | val id: String,
53 | val title: String,
54 | val color: String,
55 | val coverPhoto: String
56 | )
57 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/utils/broadcast_receiver/ShakeManager.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.utils.broadcast_receiver
2 |
3 | import android.content.Context
4 | import android.hardware.Sensor
5 | import android.hardware.SensorEvent
6 | import android.hardware.SensorEventListener
7 | import android.hardware.SensorManager
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.DisposableEffect
10 | import androidx.compose.ui.platform.LocalContext
11 |
12 | @Composable
13 | fun ShakeManager(
14 | systemAction: String,
15 | onServiceAction: () -> Unit
16 | ) {
17 | val context = LocalContext.current
18 | DisposableEffect(context, systemAction) {
19 | var acceleration = 0F
20 | var currentAcceleration = 0F
21 | var lastAcceleration = 0F
22 | val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
23 | val sensorListener = object : SensorEventListener {
24 | override fun onSensorChanged(event: SensorEvent) {
25 | val x = event.values[0]
26 | val y = event.values[1]
27 | val z = event.values[2]
28 | lastAcceleration = currentAcceleration
29 | currentAcceleration = kotlin.math.sqrt((x * x + y * y + z * z).toDouble()).toFloat()
30 | val delta: Float = currentAcceleration - lastAcceleration
31 | acceleration = acceleration * 0.9F + delta
32 | if (acceleration > 12) {
33 | onServiceAction()
34 | }
35 | }
36 |
37 | override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { }
38 | }
39 |
40 | sensorManager.registerListener(sensorListener, sensorManager.getDefaultSensor(
41 | Sensor .TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_NORMAL
42 | )
43 |
44 | onDispose {
45 | sensorManager.unregisterListener(sensorListener)
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/ui/animation/ScreensAnim.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.ui.animation
2 |
3 | import androidx.compose.animation.*
4 | import androidx.compose.animation.core.tween
5 | import androidx.compose.foundation.ExperimentalFoundationApi
6 | import androidx.compose.material.ExperimentalMaterialApi
7 | import androidx.compose.ui.unit.ExperimentalUnitApi
8 | import androidx.navigation.NavBackStackEntry
9 | import coil.annotation.ExperimentalCoilApi
10 | import com.eneskayiklik.wallup.destinations.SplashScreenDestination
11 | import com.eneskayiklik.wallup.navDestination
12 | import com.ramcosta.composedestinations.spec.DestinationStyle
13 |
14 | @ExperimentalAnimationApi
15 | @ExperimentalFoundationApi
16 | @ExperimentalMaterialApi
17 | @ExperimentalUnitApi
18 | @ExperimentalCoilApi
19 | object ScreensAnim : DestinationStyle.Animated {
20 | override fun AnimatedContentScope.enterTransition(): EnterTransition? {
21 | return when (initialState.navDestination) {
22 | SplashScreenDestination -> null
23 | else -> slideInHorizontally(animationSpec = tween(300), initialOffsetX = { it })
24 | }
25 | }
26 |
27 | override fun AnimatedContentScope.exitTransition(): ExitTransition {
28 | return slideOutHorizontally(
29 | animationSpec = tween(300),
30 | targetOffsetX = { (-it / 3) * 2 }) + fadeOut(
31 | animationSpec = tween(durationMillis = 300), targetAlpha = 0.3F
32 | )
33 | }
34 |
35 | override fun AnimatedContentScope.popEnterTransition(): EnterTransition {
36 | return slideInHorizontally(animationSpec = tween(300), initialOffsetX = { -it }) + fadeIn(
37 | animationSpec = tween(durationMillis = 300), initialAlpha = 0.3F
38 | )
39 | }
40 |
41 | override fun AnimatedContentScope.popExitTransition(): ExitTransition {
42 | return slideOutHorizontally(animationSpec = tween(300), targetOffsetX = { it })
43 | }
44 | }
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_home/presentation/component/WelcomeSection.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_home.presentation.component
2 |
3 | import androidx.compose.animation.ExperimentalAnimationApi
4 | import androidx.compose.foundation.ExperimentalFoundationApi
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.foundation.lazy.LazyListScope
7 | import androidx.compose.material.*
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.filled.Bookmarks
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.unit.ExperimentalUnitApi
13 | import androidx.compose.ui.unit.dp
14 | import coil.annotation.ExperimentalCoilApi
15 | import com.eneskayiklik.wallup.destinations.BookmarkScreenDestination
16 | import com.eneskayiklik.wallup.feature_home.domain.model.HomeEvent
17 |
18 | @ExperimentalCoilApi
19 | @ExperimentalAnimationApi
20 | @ExperimentalUnitApi
21 | @ExperimentalMaterialApi
22 | @ExperimentalFoundationApi
23 | fun LazyListScope.welcomeSection(onEvent: (HomeEvent) -> Unit) {
24 | item(key = "welcome_section") { WelcomeSection(onEvent) }
25 | }
26 |
27 | @ExperimentalCoilApi
28 | @ExperimentalUnitApi
29 | @ExperimentalAnimationApi
30 | @ExperimentalMaterialApi
31 | @ExperimentalFoundationApi
32 | @Composable
33 | private fun WelcomeSection(onEvent: (HomeEvent) -> Unit) {
34 | Row(
35 | modifier = Modifier
36 | .fillMaxWidth()
37 | .padding(horizontal = 16.dp),
38 | horizontalArrangement = Arrangement.SpaceBetween
39 | ) {
40 | Column {
41 | Text(
42 | text = "Welcome to WallUp", style = MaterialTheme.typography.h4.copy(
43 | color = MaterialTheme.colors.onBackground
44 | )
45 | )
46 | Text(
47 | text = "Discover Unsplash photos and find best wallpaper for you.",
48 | style = MaterialTheme.typography.subtitle1.copy(
49 | color = MaterialTheme.colors.onBackground
50 | )
51 | )
52 | }
53 | IconButton(modifier = Modifier.size(40.dp), onClick = {
54 | onEvent(
55 | HomeEvent.Navigate(BookmarkScreenDestination)
56 | )
57 | }) {
58 | Icon(
59 | imageVector = Icons.Default.Bookmarks,
60 | contentDescription = "Bookmarks",
61 | tint = MaterialTheme.colors.onBackground
62 | )
63 | }
64 | }
65 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_collection/presentation/CollectionScreen.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_collection.presentation
2 |
3 |
4 | import androidx.compose.animation.ExperimentalAnimationApi
5 | import androidx.compose.foundation.ExperimentalFoundationApi
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.PaddingValues
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.lazy.LazyColumn
12 | import androidx.compose.foundation.lazy.rememberLazyListState
13 | import androidx.compose.material.ExperimentalMaterialApi
14 | import androidx.compose.material.MaterialTheme
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.collectAsState
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.unit.ExperimentalUnitApi
19 | import androidx.compose.ui.unit.dp
20 | import androidx.hilt.navigation.compose.hiltViewModel
21 | import coil.annotation.ExperimentalCoilApi
22 | import com.eneskayiklik.wallup.feature_collection.domain.model.CollectionScreenNavArgs
23 | import com.eneskayiklik.wallup.feature_collection.presentation.component.itemsSection
24 | import com.eneskayiklik.wallup.feature_collection.presentation.component.titleSection
25 | import com.eneskayiklik.wallup.ui.animation.ScreensAnim
26 | import com.ramcosta.composedestinations.annotation.Destination
27 | import com.ramcosta.composedestinations.navigation.DestinationsNavigator
28 |
29 | @ExperimentalAnimationApi
30 | @ExperimentalUnitApi
31 | @Destination(
32 | navArgsDelegate = CollectionScreenNavArgs::class,
33 | style = ScreensAnim::class
34 | )
35 | @ExperimentalFoundationApi
36 | @ExperimentalMaterialApi
37 | @ExperimentalCoilApi
38 | @Composable
39 | fun CollectionScreen(
40 | navigator: DestinationsNavigator,
41 | viewModel: CollectionViewModel = hiltViewModel()
42 | ) {
43 | val state = viewModel.collectionState.collectAsState().value
44 | val scrollState = rememberLazyListState()
45 | LazyColumn(
46 | modifier = Modifier
47 | .fillMaxSize()
48 | .background(MaterialTheme.colors.background),
49 | contentPadding = PaddingValues(top = 60.dp, bottom = 16.dp),
50 | verticalArrangement = Arrangement.spacedBy(8.dp),
51 | state = scrollState
52 | ) {
53 | titleSection(state.title, state.count, modifier = Modifier.padding(horizontal = 16.dp))
54 | itemsSection(state.items, navigator)
55 | }
56 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_bookmark/presentation/BookmarkScreen.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_bookmark.presentation
2 |
3 |
4 | import androidx.compose.animation.ExperimentalAnimationApi
5 | import androidx.compose.foundation.ExperimentalFoundationApi
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.PaddingValues
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.lazy.LazyColumn
12 | import androidx.compose.foundation.lazy.rememberLazyListState
13 | import androidx.compose.material.ExperimentalMaterialApi
14 | import androidx.compose.material.MaterialTheme
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.collectAsState
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.unit.ExperimentalUnitApi
19 | import androidx.compose.ui.unit.dp
20 | import androidx.hilt.navigation.compose.hiltViewModel
21 | import coil.annotation.ExperimentalCoilApi
22 | import com.eneskayiklik.wallup.feature_bookmark.presentation.component.EmptyBookmarkSection
23 | import com.eneskayiklik.wallup.feature_bookmark.presentation.component.bookmarkSection
24 | import com.eneskayiklik.wallup.feature_collection.presentation.component.titleSection
25 | import com.eneskayiklik.wallup.ui.animation.ScreensAnim
26 | import com.ramcosta.composedestinations.annotation.Destination
27 | import com.ramcosta.composedestinations.navigation.DestinationsNavigator
28 |
29 | @ExperimentalAnimationApi
30 | @ExperimentalUnitApi
31 | @Destination(
32 | style = ScreensAnim::class
33 | )
34 | @ExperimentalFoundationApi
35 | @ExperimentalMaterialApi
36 | @ExperimentalCoilApi
37 | @Composable
38 | fun BookmarkScreen(
39 | navigator: DestinationsNavigator,
40 | viewModel: BookmarkViewModel = hiltViewModel()
41 | ) {
42 | val state = viewModel.bookmarkState.collectAsState().value
43 | val scrollState = rememberLazyListState()
44 | if (state.count == 0) {
45 | EmptyBookmarkSection()
46 | } else {
47 | LazyColumn(
48 | modifier = Modifier
49 | .fillMaxSize()
50 | .background(MaterialTheme.colors.background),
51 | contentPadding = PaddingValues(top = 60.dp, bottom = 16.dp),
52 | verticalArrangement = Arrangement.spacedBy(8.dp),
53 | state = scrollState
54 | ) {
55 | titleSection(state.title, state.count, modifier = Modifier.padding(horizontal = 16.dp))
56 | bookmarkSection(state.items) {
57 | navigator.navigate(it)
58 | }
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_home/presentation/HomeViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_home.presentation
2 |
3 | import android.util.Log
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.eneskayiklik.wallup.feature_home.domain.model.HomeEvent
7 | import com.eneskayiklik.wallup.feature_home.domain.model.HomeState
8 | import com.eneskayiklik.wallup.feature_home.domain.use_case.HomeUseCase
9 | import com.eneskayiklik.wallup.utils.model.UiEvent
10 | import com.eneskayiklik.wallup.utils.network.Resource
11 | import dagger.hilt.android.lifecycle.HiltViewModel
12 | import kotlinx.coroutines.flow.*
13 | import kotlinx.coroutines.launch
14 | import javax.inject.Inject
15 |
16 | @HiltViewModel
17 | class HomeViewModel @Inject constructor(
18 | private val useCase: HomeUseCase
19 | ) : ViewModel() {
20 |
21 | private val _homeState = MutableStateFlow(HomeState())
22 | val homeState: StateFlow = _homeState
23 |
24 | private val _uiEvent = MutableSharedFlow()
25 | val uiEvent: SharedFlow = _uiEvent
26 |
27 | init {
28 | getHomeState()
29 | }
30 |
31 | fun onEvent(event: HomeEvent) {
32 | viewModelScope.launch {
33 | when (event) {
34 | is HomeEvent.Navigate -> viewModelScope.launch {
35 | _uiEvent.emit(UiEvent.OnNavigate(event.route))
36 | }
37 | HomeEvent.ScrollTop -> viewModelScope.launch {
38 | _uiEvent.emit(UiEvent.ScrollTop)
39 | }
40 | }
41 | }
42 | }
43 |
44 | private fun getHomeState() {
45 | getColors()
46 | getCategories()
47 | getRandomPhotos()
48 | }
49 |
50 | private fun getColors() {
51 | viewModelScope.launch {
52 | useCase.getColorList().collectLatest {
53 | _homeState.value = _homeState.value.copy(
54 | colorList = it
55 | )
56 | }
57 | }
58 | }
59 |
60 | private fun getCategories() {
61 | viewModelScope.launch {
62 | useCase.getCategoryList().collectLatest {
63 | _homeState.value = _homeState.value.copy(
64 | categories = it
65 | )
66 | }
67 | }
68 | }
69 |
70 | private fun getRandomPhotos() {
71 | viewModelScope.launch {
72 | useCase.getRandomPhotos().collectLatest {
73 | when (it) {
74 | is Resource.Error -> Log.e("HomeViewModel", it.message)
75 | is Resource.Loading -> {}
76 | is Resource.Success -> {
77 | _homeState.value = _homeState.value.copy(
78 | randomPhotos = it.data
79 | )
80 | }
81 | }
82 | }
83 | }
84 | }
85 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_home/presentation/component/ColorSection.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_home.presentation.component
2 |
3 | import androidx.compose.animation.ExperimentalAnimationApi
4 | import androidx.compose.foundation.ExperimentalFoundationApi
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.foundation.lazy.LazyItemScope
7 | import androidx.compose.foundation.lazy.LazyListScope
8 | import androidx.compose.foundation.lazy.LazyRow
9 | import androidx.compose.foundation.shape.RoundedCornerShape
10 | import androidx.compose.material.ExperimentalMaterialApi
11 | import androidx.compose.material.Surface
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.unit.ExperimentalUnitApi
16 | import androidx.compose.ui.unit.dp
17 | import coil.annotation.ExperimentalCoilApi
18 | import com.eneskayiklik.wallup.destinations.CollectionScreenDestination
19 | import com.eneskayiklik.wallup.feature_home.domain.model.ColorItem
20 | import com.eneskayiklik.wallup.feature_home.domain.model.HomeEvent
21 | import com.ramcosta.composedestinations.spec.Direction
22 |
23 | @ExperimentalUnitApi
24 | @ExperimentalAnimationApi
25 | @ExperimentalCoilApi
26 | @ExperimentalMaterialApi
27 | @ExperimentalFoundationApi
28 | fun LazyListScope.colorSection(colors: List, onEvent: (HomeEvent) -> Unit) {
29 | item(key = "color_section") {
30 | ColorSection(colors = colors) {
31 | onEvent(HomeEvent.Navigate(it))
32 | }
33 | }
34 | }
35 |
36 | @ExperimentalAnimationApi
37 | @ExperimentalUnitApi
38 | @ExperimentalCoilApi
39 | @ExperimentalMaterialApi
40 | @ExperimentalFoundationApi
41 | @Composable
42 | private fun LazyItemScope.ColorSection(
43 | colors: List,
44 | onClick: (Direction) -> Unit
45 | ) {
46 | Column(
47 | modifier = Modifier.animateItemPlacement(),
48 | verticalArrangement = Arrangement.spacedBy(8.dp)
49 | ) {
50 | SectionTitle(
51 | title = "The color tone",
52 | modifier = Modifier.padding(start = 16.dp, top = 8.dp)
53 | )
54 | LazyRow(
55 | horizontalArrangement = Arrangement.spacedBy(8.dp),
56 | contentPadding = PaddingValues(horizontal = 16.dp)
57 | ) {
58 | items(count = colors.size, key = { it }) {
59 | SingleColorItem(colors[it], onClick)
60 | }
61 | }
62 | }
63 | }
64 |
65 | @ExperimentalUnitApi
66 | @ExperimentalAnimationApi
67 | @ExperimentalCoilApi
68 | @ExperimentalFoundationApi
69 | @ExperimentalMaterialApi
70 | @Composable
71 | private fun SingleColorItem(item: ColorItem, onClick: (Direction) -> Unit) {
72 | Surface(
73 | onClick = {
74 | onClick(CollectionScreenDestination(item.name, item.name))
75 | },
76 | modifier = Modifier.size(50.dp),
77 | color = Color(android.graphics.Color.parseColor(item.hexCode)),
78 | shape = RoundedCornerShape(10.dp)
79 | ) {
80 |
81 | }
82 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_detail/presentation/component/DetailImageContent.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_detail.presentation.component
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.compose.animation.core.animateFloatAsState
5 | import androidx.compose.animation.core.tween
6 | import androidx.compose.foundation.Image
7 | import androidx.compose.foundation.horizontalScroll
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.height
11 | import androidx.compose.foundation.rememberScrollState
12 | import androidx.compose.material.LinearProgressIndicator
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.getValue
15 | import androidx.compose.runtime.rememberCoroutineScope
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.draw.scale
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.layout.ContentScale
21 | import androidx.compose.ui.unit.dp
22 | import coil.annotation.ExperimentalCoilApi
23 | import coil.compose.ImagePainter
24 | import kotlinx.coroutines.launch
25 |
26 | @SuppressLint("CoroutineCreationDuringComposition")
27 | @ExperimentalCoilApi
28 | @Composable
29 | fun DetailImageContent(
30 | painter: ImagePainter,
31 | thumbnailPainter: ImagePainter,
32 | modifier: Modifier = Modifier
33 | ) {
34 | val isLoading = painter.state !is ImagePainter.State.Success
35 | val thumbnailImageScale = if (isLoading) 1.05F else 0F
36 | val actualImageScale by animateFloatAsState(
37 | targetValue = if (isLoading) 1.05F else 1F,
38 | animationSpec = tween(durationMillis = 500)
39 | )
40 | val thumbnailImageState = rememberScrollState()
41 | val actualImageState = rememberScrollState()
42 | rememberCoroutineScope().launch {
43 | if (isLoading)
44 | thumbnailImageState.scrollTo(thumbnailImageState.maxValue / 2)
45 | else
46 | actualImageState.scrollTo(actualImageState.maxValue / 2)
47 | }
48 | Image(
49 | painter = thumbnailPainter,
50 | contentScale = ContentScale.FillHeight,
51 | contentDescription = "Full Image",
52 | modifier = modifier
53 | .scale(scaleX = thumbnailImageScale, scaleY = 1F)
54 | .horizontalScroll(thumbnailImageState),
55 | )
56 | Image(
57 | painter = painter,
58 | contentScale = ContentScale.FillHeight,
59 | contentDescription = "Full Image",
60 | modifier = modifier
61 | .scale(scaleX = actualImageScale, scaleY = 1F)
62 | .horizontalScroll(actualImageState),
63 | )
64 |
65 | if (isLoading) {
66 | Box(modifier = modifier) {
67 | LinearProgressIndicator(
68 | modifier = Modifier
69 | .fillMaxWidth()
70 | .height(5.dp)
71 | .align(Alignment.BottomCenter),
72 | color = Color.White
73 | )
74 | }
75 | }
76 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_home/presentation/HomeScreen.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_home.presentation
2 |
3 | import android.content.Context
4 | import androidx.compose.animation.ExperimentalAnimationApi
5 | import androidx.compose.foundation.ExperimentalFoundationApi
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.PaddingValues
9 | import androidx.compose.foundation.lazy.LazyColumn
10 | import androidx.compose.foundation.lazy.rememberLazyListState
11 | import androidx.compose.material.ExperimentalMaterialApi
12 | import androidx.compose.material.MaterialTheme
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.LaunchedEffect
15 | import androidx.compose.runtime.collectAsState
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.unit.ExperimentalUnitApi
18 | import androidx.compose.ui.unit.dp
19 | import androidx.hilt.navigation.compose.hiltViewModel
20 | import coil.annotation.ExperimentalCoilApi
21 | import com.eneskayiklik.wallup.feature_home.domain.model.HomeEvent
22 | import com.eneskayiklik.wallup.feature_home.presentation.component.categoriesSection
23 | import com.eneskayiklik.wallup.feature_home.presentation.component.colorSection
24 | import com.eneskayiklik.wallup.feature_home.presentation.component.suggestedSection
25 | import com.eneskayiklik.wallup.feature_home.presentation.component.welcomeSection
26 | import com.eneskayiklik.wallup.ui.animation.ScreensAnim
27 | import com.eneskayiklik.wallup.utils.broadcast_receiver.ShakeManager
28 | import com.eneskayiklik.wallup.utils.model.UiEvent
29 | import com.ramcosta.composedestinations.annotation.Destination
30 | import com.ramcosta.composedestinations.navigation.DestinationsNavigator
31 | import kotlinx.coroutines.flow.collectLatest
32 |
33 | @ExperimentalUnitApi
34 | @ExperimentalAnimationApi
35 | @Destination(
36 | style = ScreensAnim::class
37 | )
38 | @ExperimentalFoundationApi
39 | @ExperimentalMaterialApi
40 | @ExperimentalCoilApi
41 | @Composable
42 | fun HomeScreen(
43 | navigator: DestinationsNavigator,
44 | viewModel: HomeViewModel = hiltViewModel()
45 | ) {
46 | val state = viewModel.homeState.collectAsState().value
47 | val scrollState = rememberLazyListState()
48 | LaunchedEffect(key1 = true) {
49 | viewModel.uiEvent.collectLatest {
50 | when (it) {
51 | is UiEvent.OnNavigate -> navigator.navigate(it.route)
52 | is UiEvent.ScrollTop -> scrollState.animateScrollToItem(0)
53 | else -> {}
54 | }
55 | }
56 | }
57 | ShakeManager(systemAction = Context.SENSOR_SERVICE) {
58 | viewModel.onEvent(HomeEvent.ScrollTop)
59 | }
60 | LazyColumn(
61 | modifier = Modifier.background(MaterialTheme.colors.background),
62 | contentPadding = PaddingValues(top = 50.dp, bottom = 16.dp),
63 | verticalArrangement = Arrangement.spacedBy(8.dp),
64 | state = scrollState
65 | ) {
66 | welcomeSection(viewModel::onEvent)
67 | suggestedSection(state.randomPhotos, viewModel::onEvent)
68 | colorSection(state.colorList, viewModel::onEvent)
69 | categoriesSection(state.categories, viewModel::onEvent)
70 | }
71 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_home/presentation/component/CategoriesSection.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_home.presentation.component
2 |
3 | import androidx.compose.animation.ExperimentalAnimationApi
4 | import androidx.compose.foundation.ExperimentalFoundationApi
5 | import androidx.compose.foundation.Image
6 | import androidx.compose.foundation.layout.*
7 | import androidx.compose.foundation.lazy.LazyListScope
8 | import androidx.compose.foundation.shape.RoundedCornerShape
9 | import androidx.compose.material.ExperimentalMaterialApi
10 | import androidx.compose.material.MaterialTheme
11 | import androidx.compose.material.Surface
12 | import androidx.compose.material.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.draw.scale
17 | import androidx.compose.ui.graphics.Color
18 | import androidx.compose.ui.layout.ContentScale
19 | import androidx.compose.ui.res.painterResource
20 | import androidx.compose.ui.unit.ExperimentalUnitApi
21 | import androidx.compose.ui.unit.dp
22 | import coil.annotation.ExperimentalCoilApi
23 | import com.eneskayiklik.wallup.destinations.CollectionScreenDestination
24 | import com.eneskayiklik.wallup.feature_home.domain.model.Category
25 | import com.eneskayiklik.wallup.feature_home.domain.model.HomeEvent
26 | import com.eneskayiklik.wallup.utils.extensions.gridItems
27 | import com.ramcosta.composedestinations.spec.Direction
28 |
29 | @ExperimentalAnimationApi
30 | @ExperimentalUnitApi
31 | @ExperimentalCoilApi
32 | @ExperimentalMaterialApi
33 | @ExperimentalFoundationApi
34 | fun LazyListScope.categoriesSection(
35 | categories: List,
36 | onEvent: (HomeEvent) -> Unit
37 | ) {
38 | item(key = "categories_section") {
39 | SectionTitle(
40 | title = "Categories",
41 | modifier = Modifier
42 | .padding(start = 16.dp, top = 8.dp)
43 | )
44 | }
45 | gridItems(
46 | data = categories,
47 | columnCount = 2,
48 | horizontalArrangement = Arrangement.spacedBy(8.dp),
49 | modifier = Modifier.padding(horizontal = 16.dp)
50 | ) {
51 | SingleCategoryItem(it) { route ->
52 | onEvent(HomeEvent.Navigate(route))
53 | }
54 | }
55 | }
56 |
57 | @ExperimentalUnitApi
58 | @ExperimentalAnimationApi
59 | @ExperimentalCoilApi
60 | @ExperimentalFoundationApi
61 | @ExperimentalMaterialApi
62 | @Composable
63 | private fun SingleCategoryItem(item: Category, onClick: (Direction) -> Unit) {
64 | Surface(
65 | onClick = { onClick(CollectionScreenDestination(item.title, item.title)) },
66 | modifier = Modifier
67 | .fillMaxWidth()
68 | .height(85.dp),
69 | shape = RoundedCornerShape(10.dp)
70 | ) {
71 | Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
72 | Image(
73 | painter = painterResource(id = item.imageRes),
74 | contentDescription = item.title,
75 | contentScale = ContentScale.Crop,
76 | modifier = Modifier
77 | .fillMaxSize()
78 | .scale(2F)
79 | )
80 | Text(
81 | text = item.title,
82 | style = MaterialTheme.typography.h6.copy(
83 | color = Color.White
84 | )
85 | )
86 | }
87 | }
88 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_collection/presentation/CollectionViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_collection.presentation
2 |
3 | import android.util.Log
4 | import androidx.lifecycle.SavedStateHandle
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.eneskayiklik.wallup.feature_collection.domain.use_case.CollectionUseCase
8 | import com.eneskayiklik.wallup.feature_collection.domain.model.CollectionState
9 | import com.eneskayiklik.wallup.feature_collection.domain.use_case.SearchUseCase
10 | import com.eneskayiklik.wallup.utils.network.Resource
11 | import dagger.hilt.android.lifecycle.HiltViewModel
12 | import kotlinx.coroutines.flow.MutableStateFlow
13 | import kotlinx.coroutines.flow.StateFlow
14 | import kotlinx.coroutines.flow.collectLatest
15 | import kotlinx.coroutines.launch
16 | import javax.inject.Inject
17 |
18 | @HiltViewModel
19 | class CollectionViewModel @Inject constructor(
20 | private val searchUseCase: SearchUseCase,
21 | private val collectionUseCase: CollectionUseCase,
22 | args: SavedStateHandle
23 | ): ViewModel() {
24 |
25 | private val _collectionState = MutableStateFlow(CollectionState())
26 | val collectionState: StateFlow = _collectionState
27 |
28 | init {
29 | checkArgumentsAndMakeRequest(
30 | args.get("collectionId"),
31 | args.get("searchQuery"),
32 | args.get("title")
33 | )
34 | }
35 |
36 | private fun checkArgumentsAndMakeRequest(
37 | collectionId: String?,
38 | searchQuery: String?,
39 | title: String?
40 | ) {
41 | if (title != null || searchQuery != null) {
42 | _collectionState.value = _collectionState.value.copy(
43 | title = (title ?: searchQuery) ?: ""
44 | )
45 | }
46 | if (collectionId != null) {
47 | getCollectionResponse(collectionId)
48 | } else if (searchQuery != null) {
49 | getSearchResponse(searchQuery)
50 | }
51 | }
52 |
53 | private fun getSearchResponse(query: String) {
54 | viewModelScope.launch {
55 | searchUseCase.getSearchData(query).collectLatest {
56 | when (it) {
57 | is Resource.Error -> Log.e("CollectionViewModel", it.message)
58 | is Resource.Loading -> { }
59 | is Resource.Success -> {
60 | _collectionState.value = _collectionState.value.copy(
61 | items = it.data,
62 | count = it.data.count()
63 | )
64 | }
65 | }
66 | }
67 | }
68 | }
69 |
70 | private fun getCollectionResponse(collectionId: String) {
71 | viewModelScope.launch {
72 | collectionUseCase.getCollectionData(collectionId).collectLatest {
73 | when (it) {
74 | is Resource.Error -> Log.e("CollectionViewModel", it.message)
75 | is Resource.Loading -> { }
76 | is Resource.Success -> {
77 | _collectionState.value = _collectionState.value.copy(
78 | items = it.data,
79 | count = it.data.count()
80 | )
81 | }
82 | }
83 | }
84 | }
85 | }
86 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_detail/presentation/component/DetailImageItem.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_detail.presentation.component
2 |
3 | import androidx.compose.animation.ExperimentalAnimationApi
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.layout.size
9 | import androidx.compose.foundation.lazy.LazyListScope
10 | import androidx.compose.material.ExperimentalMaterialApi
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.platform.LocalConfiguration
16 | import androidx.compose.ui.unit.dp
17 | import coil.annotation.ExperimentalCoilApi
18 | import coil.compose.ImagePainter
19 | import coil.compose.rememberImagePainter
20 | import com.eneskayiklik.wallup.feature_detail.domain.model.DetailEvent
21 | import com.eneskayiklik.wallup.feature_detail.domain.model.DetailState
22 |
23 | @ExperimentalCoilApi
24 | @ExperimentalAnimationApi
25 | @ExperimentalMaterialApi
26 | fun LazyListScope.imageItem(
27 | mColor: Color,
28 | thumbnail: String?,
29 | detailState: DetailState,
30 | onEvent: (DetailEvent) -> Unit
31 | ) {
32 | item {
33 | ImageItem(
34 | mColor = mColor,
35 | thumbnail = thumbnail,
36 | detailState = detailState,
37 | onEvent = onEvent
38 | )
39 | }
40 | }
41 |
42 | @ExperimentalCoilApi
43 | @ExperimentalMaterialApi
44 | @ExperimentalAnimationApi
45 | @Composable
46 | private fun ImageItem(
47 | mColor: Color,
48 | thumbnail: String?,
49 | detailState: DetailState,
50 | onEvent: (DetailEvent) -> Unit
51 | ) {
52 | val localeConfig = LocalConfiguration.current
53 | Box(
54 | modifier = Modifier
55 | .size(
56 | width = localeConfig.screenWidthDp.dp + 5.dp,
57 | height = localeConfig.screenHeightDp.dp - 48.dp
58 | )
59 | .background(mColor)
60 | ) {
61 | val imageUrl = detailState.imageDetail?.fullImage
62 | val painter = rememberImagePainter(data = imageUrl)
63 | val thumbnailPainter = rememberImagePainter(data = thumbnail)
64 | onEvent(DetailEvent.UpdateDrawable((painter.state as? ImagePainter.State.Success)?.result?.drawable))
65 | DetailImageContent(
66 | painter = painter,
67 | thumbnailPainter = thumbnailPainter,
68 | modifier = Modifier
69 | .fillMaxSize()
70 | .align(Alignment.Center)
71 | )
72 | DetailButtonStack(
73 | modifier = Modifier
74 | .align(Alignment.BottomCenter)
75 | .padding(vertical = 24.dp),
76 | imageUrl = imageUrl ?: "",
77 | thumbnailUrl = thumbnail ?: "",
78 | isBookmarked = detailState.imageDetail?.isBookmarked ?: false,
79 | imageId = detailState.imageDetail?.id ?: "",
80 | imageBitmap = detailState.imageDrawable,
81 | isDownloading = detailState.currentDownloadId != null,
82 | createdAt = detailState.imageDetail?.createdAt ?: "",
83 | isVisible = painter.state is ImagePainter.State.Success,
84 | buttonsBackColor = detailState.imageDetail?.color,
85 | onEvent = onEvent
86 | )
87 | }
88 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_bookmark/presentation/component/BookmarkSection.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_bookmark.presentation.component
2 |
3 | import androidx.compose.animation.ExperimentalAnimationApi
4 | import androidx.compose.animation.core.Spring
5 | import androidx.compose.animation.core.animateFloatAsState
6 | import androidx.compose.animation.core.spring
7 | import androidx.compose.foundation.ExperimentalFoundationApi
8 | import androidx.compose.foundation.Image
9 | import androidx.compose.foundation.layout.Arrangement
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.size
12 | import androidx.compose.foundation.lazy.LazyListScope
13 | import androidx.compose.foundation.shape.RoundedCornerShape
14 | import androidx.compose.material.ExperimentalMaterialApi
15 | import androidx.compose.material.Surface
16 | import androidx.compose.runtime.*
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.draw.scale
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.layout.ContentScale
21 | import androidx.compose.ui.unit.ExperimentalUnitApi
22 | import androidx.compose.ui.unit.dp
23 | import coil.annotation.ExperimentalCoilApi
24 | import coil.compose.rememberImagePainter
25 | import com.eneskayiklik.wallup.destinations.DetailScreenDestination
26 | import com.eneskayiklik.wallup.feature_bookmark.data.db.entity.BookmarkPhoto
27 | import com.eneskayiklik.wallup.utils.extensions.gridItems
28 | import com.ramcosta.composedestinations.spec.Direction
29 | import java.net.URLEncoder
30 | import java.nio.charset.StandardCharsets
31 |
32 | @ExperimentalUnitApi
33 | @ExperimentalAnimationApi
34 | @ExperimentalCoilApi
35 | @ExperimentalMaterialApi
36 | @ExperimentalFoundationApi
37 | fun LazyListScope.bookmarkSection(
38 | categories: List,
39 | onNavigate: (Direction) -> Unit
40 | ) {
41 | gridItems(
42 | data = categories,
43 | columnCount = 2,
44 | horizontalArrangement = Arrangement.spacedBy(8.dp),
45 | modifier = Modifier.padding(horizontal = 16.dp)
46 | ) {
47 | SingleBookmarkItem(it, onNavigate)
48 | }
49 | }
50 |
51 | @ExperimentalAnimationApi
52 | @ExperimentalUnitApi
53 | @ExperimentalFoundationApi
54 | @ExperimentalCoilApi
55 | @ExperimentalMaterialApi
56 | @Composable
57 | private fun SingleBookmarkItem(data: BookmarkPhoto, onClick: (Direction) -> Unit) {
58 | var isClicked by remember { mutableStateOf(false) }
59 | val scaleAnim by animateFloatAsState(
60 | targetValue = if (isClicked) 1.05F else 1F,
61 | animationSpec = spring(
62 | stiffness = Spring.StiffnessLow,
63 | dampingRatio = Spring.DampingRatioLowBouncy
64 | ),
65 | )
66 | Surface(
67 | modifier = Modifier
68 | .size(width = 150.dp, height = 250.dp)
69 | .scale(scaleAnim),
70 | onClick = {
71 | isClicked = isClicked.not()
72 | val encodedUrl = URLEncoder.encode(data.thumbnail, StandardCharsets.UTF_8.toString())
73 | onClick(DetailScreenDestination(data.unsplashId, thumbnail = encodedUrl))
74 | },
75 | elevation = 2.dp,
76 | color = Color(android.graphics.Color.parseColor(data.color)),
77 | shape = RoundedCornerShape(10.dp)
78 | ) {
79 | val painter = rememberImagePainter(data = data.thumbnail) {
80 | crossfade(350)
81 | }
82 |
83 | Image(
84 | painter = painter,
85 | contentScale = ContentScale.Crop,
86 | contentDescription = "Bookmark Photo"
87 | )
88 | }
89 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_collection/presentation/component/ItemsSection.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_collection.presentation.component
2 |
3 | import androidx.compose.animation.ExperimentalAnimationApi
4 | import androidx.compose.animation.core.Spring
5 | import androidx.compose.animation.core.animateFloatAsState
6 | import androidx.compose.animation.core.spring
7 | import androidx.compose.foundation.ExperimentalFoundationApi
8 | import androidx.compose.foundation.Image
9 | import androidx.compose.foundation.layout.Arrangement
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.size
12 | import androidx.compose.foundation.lazy.LazyListScope
13 | import androidx.compose.foundation.shape.RoundedCornerShape
14 | import androidx.compose.material.ExperimentalMaterialApi
15 | import androidx.compose.material.Surface
16 | import androidx.compose.runtime.*
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.draw.scale
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.layout.ContentScale
21 | import androidx.compose.ui.unit.ExperimentalUnitApi
22 | import androidx.compose.ui.unit.dp
23 | import coil.annotation.ExperimentalCoilApi
24 | import coil.compose.rememberImagePainter
25 | import com.eneskayiklik.wallup.destinations.DetailScreenDestination
26 | import com.eneskayiklik.wallup.feature_home.domain.model.UnsplashPhoto
27 | import com.eneskayiklik.wallup.utils.extensions.gridItems
28 | import com.ramcosta.composedestinations.navigation.DestinationsNavigator
29 | import java.net.URLEncoder
30 | import java.nio.charset.StandardCharsets
31 |
32 | @ExperimentalUnitApi
33 | @ExperimentalAnimationApi
34 | @ExperimentalCoilApi
35 | @ExperimentalMaterialApi
36 | @ExperimentalFoundationApi
37 | fun LazyListScope.itemsSection(
38 | categories: List,
39 | navigator: DestinationsNavigator
40 | ) {
41 | gridItems(
42 | data = categories,
43 | columnCount = 2,
44 | horizontalArrangement = Arrangement.spacedBy(8.dp),
45 | modifier = Modifier.padding(horizontal = 16.dp)
46 | ) {
47 | SingleSearchResultItem(it, navigator)
48 | }
49 | }
50 |
51 | @ExperimentalAnimationApi
52 | @ExperimentalUnitApi
53 | @ExperimentalFoundationApi
54 | @ExperimentalCoilApi
55 | @ExperimentalMaterialApi
56 | @Composable
57 | private fun SingleSearchResultItem(data: UnsplashPhoto, navigator: DestinationsNavigator) {
58 | var isClicked by remember { mutableStateOf(false) }
59 | val scaleAnim by animateFloatAsState(
60 | targetValue = if (isClicked) 1.05F else 1F,
61 | animationSpec = spring(
62 | stiffness = Spring.StiffnessLow,
63 | dampingRatio = Spring.DampingRatioLowBouncy
64 | ),
65 | )
66 | Surface(
67 | modifier = Modifier
68 | .size(width = 150.dp, height = 250.dp)
69 | .scale(scaleAnim),
70 | onClick = {
71 | isClicked = isClicked.not()
72 | val encodedUrl = URLEncoder.encode(data.smallImage, StandardCharsets.UTF_8.toString())
73 | navigator.navigate(DetailScreenDestination(id = data.id, thumbnail = encodedUrl))
74 | },
75 | elevation = 2.dp,
76 | color = Color(android.graphics.Color.parseColor(data.color)),
77 | shape = RoundedCornerShape(10.dp)
78 | ) {
79 | val painter = rememberImagePainter(data = data.smallImage) {
80 | crossfade(350)
81 | }
82 |
83 | Image(
84 | painter = painter,
85 | contentScale = ContentScale.Crop,
86 | contentDescription = data.userDetail.name
87 | )
88 | }
89 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_detail/presentation/DetailScreen.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_detail.presentation
2 |
3 | import android.app.DownloadManager
4 | import android.widget.Toast
5 | import androidx.compose.animation.ExperimentalAnimationApi
6 | import androidx.compose.animation.animateColorAsState
7 | import androidx.compose.foundation.ExperimentalFoundationApi
8 | import androidx.compose.foundation.background
9 | import androidx.compose.foundation.layout.PaddingValues
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.foundation.lazy.LazyColumn
12 | import androidx.compose.material.ExperimentalMaterialApi
13 | import androidx.compose.material.MaterialTheme
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.LaunchedEffect
16 | import androidx.compose.runtime.collectAsState
17 | import androidx.compose.runtime.getValue
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.platform.LocalContext
21 | import androidx.compose.ui.unit.ExperimentalUnitApi
22 | import androidx.compose.ui.unit.dp
23 | import androidx.hilt.navigation.compose.hiltViewModel
24 | import coil.annotation.ExperimentalCoilApi
25 | import com.eneskayiklik.wallup.feature_detail.domain.model.DetailScreenNavArgs
26 | import com.eneskayiklik.wallup.feature_detail.presentation.component.imageInfoItem
27 | import com.eneskayiklik.wallup.feature_detail.presentation.component.imageItem
28 | import com.eneskayiklik.wallup.ui.animation.ScreensAnim
29 | import com.eneskayiklik.wallup.utils.broadcast_receiver.SystemBroadcastReceiver
30 | import com.eneskayiklik.wallup.utils.model.UiEvent
31 | import com.ramcosta.composedestinations.annotation.Destination
32 | import com.ramcosta.composedestinations.navigation.DestinationsNavigator
33 | import kotlinx.coroutines.flow.collectLatest
34 |
35 | @ExperimentalFoundationApi
36 | @Destination(
37 | navArgsDelegate = DetailScreenNavArgs::class,
38 | style = ScreensAnim::class
39 | )
40 | @ExperimentalUnitApi
41 | @ExperimentalAnimationApi
42 | @ExperimentalMaterialApi
43 | @ExperimentalCoilApi
44 | @Composable
45 | fun DetailScreen(
46 | navigator: DestinationsNavigator,
47 | viewModel: DetailViewModel = hiltViewModel()
48 | ) {
49 | val context = LocalContext.current
50 | val detailState = viewModel.detailState.collectAsState().value
51 | val color = detailState.imageDetail?.color
52 | val thumbnail = detailState.thumbnail ?: detailState.imageDetail?.smallImage
53 | val mColor by animateColorAsState(
54 | targetValue = if (color.isNullOrEmpty()
55 | .not()
56 | ) Color(android.graphics.Color.parseColor(color)) else MaterialTheme.colors.background
57 | )
58 | LaunchedEffect(key1 = true) {
59 | viewModel.uiEvent.collectLatest {
60 | when (it) {
61 | is UiEvent.OnNavigate -> navigator.navigate(it.route)
62 | is UiEvent.ShowToast -> Toast.makeText(context, it.title, Toast.LENGTH_LONG).show()
63 | UiEvent.PopBack -> navigator.popBackStack()
64 | else -> {}
65 | }
66 | }
67 | }
68 | SystemBroadcastReceiver(
69 | systemAction = DownloadManager.ACTION_DOWNLOAD_COMPLETE,
70 | ) { intent ->
71 | val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1L)
72 | if (id == viewModel.mDownloadId && id != null) {
73 | viewModel.downloadComplete()
74 | }
75 | }
76 | LazyColumn(
77 | modifier = Modifier
78 | .background(MaterialTheme.colors.background)
79 | .fillMaxSize(),
80 | contentPadding = PaddingValues(bottom = 12.dp)
81 | ) {
82 | imageItem(mColor, thumbnail, detailState, viewModel::onEvent)
83 | if (detailState.imageDetail != null) {
84 | imageInfoItem(detailState, viewModel::onEvent)
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_detail/presentation/component/DetailRelatedCollection.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_detail.presentation.component
2 |
3 | import androidx.compose.animation.core.Spring
4 | import androidx.compose.animation.core.animateFloatAsState
5 | import androidx.compose.animation.core.spring
6 | import androidx.compose.foundation.Image
7 | import androidx.compose.foundation.layout.*
8 | import androidx.compose.foundation.lazy.LazyListScope
9 | import androidx.compose.foundation.shape.RoundedCornerShape
10 | import androidx.compose.material.ExperimentalMaterialApi
11 | import androidx.compose.material.MaterialTheme
12 | import androidx.compose.material.Surface
13 | import androidx.compose.material.Text
14 | import androidx.compose.runtime.*
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.draw.scale
18 | import androidx.compose.ui.geometry.Offset
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.graphics.Shadow
21 | import androidx.compose.ui.layout.ContentScale
22 | import androidx.compose.ui.unit.dp
23 | import coil.annotation.ExperimentalCoilApi
24 | import coil.compose.rememberImagePainter
25 | import com.eneskayiklik.wallup.feature_detail.domain.model.DetailEvent
26 | import com.eneskayiklik.wallup.feature_home.domain.model.RelatedCollectionResult
27 | import com.eneskayiklik.wallup.feature_home.domain.model.RelatedCollections
28 | import com.eneskayiklik.wallup.feature_home.presentation.component.SectionTitle
29 |
30 | @ExperimentalCoilApi
31 | @ExperimentalMaterialApi
32 | fun LazyListScope.detailRelatedCollection(
33 | relatedCollection: RelatedCollections?,
34 | onEvent: (DetailEvent) -> Unit
35 | ) {
36 | if (relatedCollection == null)
37 | return
38 | item {
39 | SectionTitle(
40 | title = "Related Collections",
41 | modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp)
42 | )
43 | }
44 | items(relatedCollection.total, key = { it.hashCode() }) {
45 | SingleRelatedCollectionItem(data = relatedCollection.collections[it]) { id, title ->
46 | onEvent(DetailEvent.Navigate(id, title))
47 | }
48 | }
49 | }
50 |
51 | @ExperimentalCoilApi
52 | @ExperimentalMaterialApi
53 | @Composable
54 | private fun SingleRelatedCollectionItem(
55 | data: RelatedCollectionResult,
56 | onClick: (id: String, title: String) -> Unit
57 | ) {
58 | var isClicked by remember { mutableStateOf(false) }
59 | val scaleAnim by animateFloatAsState(
60 | targetValue = if (isClicked) 1.05F else 1F,
61 | animationSpec = spring(
62 | stiffness = Spring.StiffnessLow,
63 | dampingRatio = Spring.DampingRatioLowBouncy
64 | ),
65 | )
66 | Surface(
67 | modifier = Modifier
68 | .fillMaxWidth()
69 | .height(200.dp)
70 | .padding(horizontal = 16.dp, vertical = 4.dp)
71 | .scale(scaleAnim),
72 | onClick = {
73 | isClicked = isClicked.not()
74 | onClick(data.id, data.title)
75 | },
76 | elevation = 2.dp,
77 | color = Color(android.graphics.Color.parseColor(data.color)),
78 | shape = RoundedCornerShape(10.dp)
79 | ) {
80 | val painter = rememberImagePainter(data = data.coverPhoto) {
81 | crossfade(350)
82 | }
83 | Box(modifier = Modifier.fillMaxSize()) {
84 | Image(
85 | painter = painter,
86 | contentScale = ContentScale.Crop,
87 | contentDescription = data.title
88 | )
89 |
90 | Text(
91 | modifier = Modifier
92 | .align(Alignment.BottomStart)
93 | .padding(16.dp),
94 | text = data.title,
95 | color = Color.White,
96 | style = MaterialTheme.typography.h6.copy(
97 | shadow = Shadow(Color.Black, offset = Offset(1F, 1F))
98 | )
99 | )
100 | }
101 | }
102 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_empty_bookmark.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
11 |
16 |
21 |
26 |
31 |
32 |
35 |
36 |
37 |
40 |
43 |
46 |
47 |
50 |
51 |
52 |
55 |
58 |
59 |
64 |
67 |
72 |
77 |
82 |
87 |
92 |
97 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_home/data/dto/UnsplashPhotoDto.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_home.data.dto
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class UnsplashPhotoDto(
7 | val alt_description: String? = null,
8 | val blur_hash: String? = null,
9 | // val categories: List<@Contextual Any>,
10 | val color: String? = null,
11 | val created_at: String? = null,
12 | val related_collections: RelatedCollection? = null,
13 | val description: String? = null,
14 | val downloads: Int? = null,
15 | val height: Int? = null,
16 | val id: String? = null,
17 | val liked_by_user: Boolean? = null,
18 | val likes: Int? = null,
19 | val links: Links? = null,
20 | val location: Location? = null,
21 | val promoted_at: String? = null,
22 | val updated_at: String? = null,
23 | val urls: Urls? = null,
24 | val user: User? = null,
25 | val views: Int? = null,
26 | val width: Int? = null,
27 | val exif: Exif? = null
28 | )
29 |
30 | @Serializable
31 | data class User(
32 | val accepted_tos: Boolean? = null,
33 | val bio: String? = null,
34 | val first_name: String? = null,
35 | val for_hire: Boolean? = null,
36 | val id: String? = null,
37 | val instagram_username: String? = null,
38 | val last_name: String? = null,
39 | val links: LinksX? = null,
40 | val location: String? = null,
41 | val name: String? = null,
42 | val portfolio_url: String? = null,
43 | val profile_image: ProfileImage? = null,
44 | val social: Social? = null,
45 | val total_collections: Int? = null,
46 | val total_likes: Int? = null,
47 | val total_photos: Int? = null,
48 | val twitter_username: String? = null,
49 | val updated_at: String? = null,
50 | val username: String? = null
51 | )
52 |
53 | @Serializable
54 | data class Urls(
55 | val full: String? = null,
56 | val raw: String? = null,
57 | val regular: String? = null,
58 | val small: String? = null,
59 | val thumb: String? = null
60 | )
61 |
62 | @Serializable
63 | data class Social(
64 | val instagram_username: String? = null,
65 | val paypal_email: String? = null,
66 | val portfolio_url: String? = null,
67 | val twitter_username: String? = null
68 | )
69 |
70 | @Serializable
71 | data class ProfileImage(
72 | val large: String? = null,
73 | val medium: String? = null,
74 | val small: String? = null
75 | )
76 |
77 | @Serializable
78 | data class Position(
79 | val latitude: Double? = null,
80 | val longitude: Double? = null
81 | )
82 |
83 | @Serializable
84 | data class Location(
85 | val city: String? = null,
86 | val country: String? = null,
87 | val name: String? = null,
88 | val position: Position? = null,
89 | val title: String? = null
90 | )
91 |
92 | @Serializable
93 | data class LinksX(
94 | val followers: String? = null,
95 | val following: String? = null,
96 | val html: String? = null,
97 | val likes: String? = null,
98 | val photos: String? = null,
99 | val portfolio: String? = null,
100 | val self: String? = null
101 | )
102 |
103 | @Serializable
104 | data class Links(
105 | val download: String? = null,
106 | val download_location: String? = null,
107 | val html: String? = null,
108 | val self: String? = null
109 | )
110 |
111 | @Serializable
112 | data class RelatedCollection(
113 | val results: List? = null,
114 | val total: Int? = null,
115 | val type: String? = null
116 | )
117 |
118 | @Serializable
119 | data class RelatedResult(
120 | val id: String? = null,
121 | val title: String? = null,
122 | val total_photos: Int? = null,
123 | val cover_photo: CoverPhoto? = null,
124 | )
125 |
126 | @Serializable
127 | data class CoverPhoto(
128 | val blur_hash: String? = null,
129 | val color: String? = null,
130 | val urls: Urls? = null,
131 | )
132 |
133 | @Serializable
134 | data class Exif(
135 | val aperture: String? = null,
136 | val exposure_time: String? = null,
137 | val focal_length: String? = null,
138 | val iso: Int? = null,
139 | val make: String? = null,
140 | val model: String? = null,
141 | val name: String? = null
142 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_detail/presentation/component/LoadingAnim.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_detail.presentation.component
2 |
3 | import androidx.compose.animation.core.*
4 | import androidx.compose.foundation.Canvas
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.runtime.*
7 | import androidx.compose.ui.Alignment
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.geometry.CornerRadius
10 | import androidx.compose.ui.geometry.Offset
11 | import androidx.compose.ui.geometry.Size
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.tooling.preview.Preview
14 | import androidx.compose.ui.unit.Dp
15 | import androidx.compose.ui.unit.dp
16 |
17 | @Composable
18 | fun LoadingAnim(
19 | color: Color,
20 | modifier: Modifier = Modifier,
21 | lineWidth: Dp = 5.dp,
22 | lineSpace: Dp = 4.dp,
23 | lineRadius: Dp = lineWidth,
24 | animationDuration: Int = 500,
25 | lineMinSize: Dp = 20.dp,
26 | lineMaxSize: Dp = 100.dp
27 | ) {
28 | val infiniteTransition = rememberInfiniteTransition()
29 | val heightAnim by infiniteTransition.animateFloat(
30 | initialValue = lineMinSize.value,
31 | targetValue = lineMaxSize.value,
32 | animationSpec = infiniteRepeatable(
33 | animation = tween(animationDuration, easing = LinearEasing),
34 | repeatMode = RepeatMode.Reverse
35 | )
36 | )
37 | val heightAnimSecond by infiniteTransition.animateFloat(
38 | initialValue = lineMaxSize.value,
39 | targetValue = lineMinSize.value,
40 | animationSpec = infiniteRepeatable(
41 | animation = tween(animationDuration, easing = LinearEasing),
42 | repeatMode = RepeatMode.Reverse
43 | )
44 | )
45 | val topLeftAnim by infiniteTransition.animateFloat(
46 | initialValue = lineMaxSize.value / 2 - lineMinSize.value / 2,
47 | targetValue = 0F,
48 | animationSpec = infiniteRepeatable(
49 | animation = tween(animationDuration, easing = LinearEasing),
50 | repeatMode = RepeatMode.Reverse
51 | )
52 | )
53 | val topLeftAnimSecond by infiniteTransition.animateFloat(
54 | initialValue = 0F,
55 | targetValue = lineMaxSize.value / 2 - lineMinSize.value / 2,
56 | animationSpec = infiniteRepeatable(
57 | animation = tween(animationDuration, easing = LinearEasing),
58 | repeatMode = RepeatMode.Reverse
59 | )
60 | )
61 | Canvas(modifier = modifier.size(width = lineWidth * 6 + lineSpace * 5, height = lineMaxSize)) {
62 | val value = size.width / 6
63 | val lineWidthPx = lineWidth.toPx()
64 | val lineRadiusPx = lineRadius.toPx()
65 | drawRoundRect(
66 | color = color,
67 | topLeft = Offset(0F, topLeftAnim),
68 | size = Size(lineWidthPx, heightAnim),
69 | cornerRadius = CornerRadius(x = lineRadiusPx, y = lineRadiusPx),
70 | )
71 | drawRoundRect(
72 | color = color,
73 | topLeft = Offset(value, topLeftAnimSecond),
74 | size = Size(lineWidthPx, heightAnimSecond),
75 | cornerRadius = CornerRadius(x = lineRadiusPx, y = lineRadiusPx),
76 | )
77 | drawRoundRect(
78 | color = color,
79 | topLeft = Offset(2 * value, topLeftAnim),
80 | size = Size(lineWidthPx, heightAnim),
81 | cornerRadius = CornerRadius(x = lineRadiusPx, y = lineRadiusPx),
82 | )
83 | drawRoundRect(
84 | color = color,
85 | topLeft = Offset(3 * value, topLeftAnimSecond),
86 | size = Size(lineWidthPx, heightAnimSecond),
87 | cornerRadius = CornerRadius(x = lineRadiusPx, y = lineRadiusPx),
88 | )
89 | drawRoundRect(
90 | color = color,
91 | topLeft = Offset(4 * value, topLeftAnim),
92 | size = Size(lineWidthPx, heightAnim),
93 | cornerRadius = CornerRadius(x = lineRadiusPx, y = lineRadiusPx),
94 | )
95 | }
96 | }
97 |
98 | @Preview
99 | @Composable
100 | fun LoadingPrev() {
101 | Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
102 | LoadingAnim(Color.Blue)
103 | }
104 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_splash/presentation/SplashScreen.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_splash.presentation
2 |
3 | import androidx.compose.animation.ExperimentalAnimationApi
4 | import androidx.compose.animation.core.animateFloatAsState
5 | import androidx.compose.animation.core.tween
6 | import androidx.compose.foundation.ExperimentalFoundationApi
7 | import androidx.compose.foundation.Image
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.material.ExperimentalMaterialApi
12 | import androidx.compose.material.MaterialTheme
13 | import androidx.compose.material.Text
14 | import androidx.compose.runtime.*
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.draw.blur
18 | import androidx.compose.ui.draw.scale
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.layout.ContentScale
21 | import androidx.compose.ui.res.painterResource
22 | import androidx.compose.ui.res.stringResource
23 | import androidx.compose.ui.text.SpanStyle
24 | import androidx.compose.ui.text.buildAnnotatedString
25 | import androidx.compose.ui.text.font.FontWeight
26 | import androidx.compose.ui.text.style.TextDecoration
27 | import androidx.compose.ui.unit.ExperimentalUnitApi
28 | import androidx.compose.ui.unit.dp
29 | import androidx.compose.ui.unit.sp
30 | import coil.annotation.ExperimentalCoilApi
31 | import com.eneskayiklik.wallup.R
32 | import com.eneskayiklik.wallup.destinations.HomeScreenDestination
33 | import com.eneskayiklik.wallup.destinations.SplashScreenDestination
34 | import com.eneskayiklik.wallup.utils.const.UNSPLASH_URL
35 | import com.ramcosta.composedestinations.annotation.Destination
36 | import com.ramcosta.composedestinations.navigation.DestinationsNavigator
37 |
38 | @ExperimentalAnimationApi
39 | @ExperimentalUnitApi
40 | @ExperimentalCoilApi
41 | @ExperimentalMaterialApi
42 | @ExperimentalFoundationApi
43 | @Destination(
44 | start = true
45 | )
46 | @Composable
47 | fun SplashScreen(
48 | navigator: DestinationsNavigator
49 | ) {
50 | var isStarted by remember { mutableStateOf(false) }
51 | val animateScale by animateFloatAsState(
52 | targetValue = if (isStarted) 1.2F else 1F,
53 | animationSpec = tween(durationMillis = 1500)
54 | ) {
55 | navigator.navigate(HomeScreenDestination) {
56 | popUpTo(SplashScreenDestination.route) {
57 | inclusive = true
58 | }
59 | }
60 | }
61 | LaunchedEffect(key1 = true, block = { isStarted = true })
62 | Box(modifier = Modifier.fillMaxSize()) {
63 | Image(
64 | modifier = Modifier
65 | .fillMaxSize()
66 | .scale(animateScale)
67 | .blur(radius = 25.dp),
68 | painter = painterResource(id = R.drawable.ic_splash),
69 | contentScale = ContentScale.Crop,
70 | contentDescription = "Splash Image"
71 | )
72 | Text(
73 | text = stringResource(id = R.string.app_name),
74 | fontSize = 48.sp,
75 | style = MaterialTheme.typography.h6,
76 | fontWeight = FontWeight.Bold,
77 | color = MaterialTheme.colors.background,
78 | modifier = Modifier.align(Alignment.Center)
79 | )
80 | val description = buildAnnotatedString {
81 | append("Photos provided by Unsplash")
82 | addStyle(
83 | style = SpanStyle(
84 | textDecoration = TextDecoration.Underline,
85 | fontSize = 16.sp
86 | ),
87 | start = 19,
88 | end = 27
89 | )
90 | addStringAnnotation(
91 | tag = "Unsplash",
92 | annotation = UNSPLASH_URL,
93 | start = 19,
94 | end = 27
95 | )
96 | }
97 | Text(
98 | text = description,
99 | fontSize = 14.sp,
100 | fontWeight = FontWeight.SemiBold,
101 | color = Color.White,
102 | style = MaterialTheme.typography.body2,
103 | modifier = Modifier
104 | .align(Alignment.BottomCenter)
105 | .padding(bottom = 15.dp)
106 | )
107 | }
108 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_home/presentation/component/SuggestedSection.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_home.presentation.component
2 |
3 | import androidx.compose.animation.ExperimentalAnimationApi
4 | import androidx.compose.animation.core.Spring
5 | import androidx.compose.animation.core.animateFloatAsState
6 | import androidx.compose.animation.core.spring
7 | import androidx.compose.foundation.ExperimentalFoundationApi
8 | import androidx.compose.foundation.Image
9 | import androidx.compose.foundation.layout.*
10 | import androidx.compose.foundation.lazy.LazyItemScope
11 | import androidx.compose.foundation.lazy.LazyListScope
12 | import androidx.compose.foundation.lazy.LazyRow
13 | import androidx.compose.foundation.shape.RoundedCornerShape
14 | import androidx.compose.material.ExperimentalMaterialApi
15 | import androidx.compose.material.Surface
16 | import androidx.compose.runtime.*
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.draw.scale
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.layout.ContentScale
21 | import androidx.compose.ui.unit.ExperimentalUnitApi
22 | import androidx.compose.ui.unit.dp
23 | import coil.annotation.ExperimentalCoilApi
24 | import coil.compose.rememberImagePainter
25 | import com.eneskayiklik.wallup.destinations.DetailScreenDestination
26 | import com.eneskayiklik.wallup.feature_home.domain.model.HomeEvent
27 | import com.eneskayiklik.wallup.feature_home.domain.model.UnsplashPhoto
28 | import com.ramcosta.composedestinations.spec.Direction
29 | import java.net.URLEncoder
30 | import java.nio.charset.StandardCharsets
31 |
32 | @ExperimentalAnimationApi
33 | @ExperimentalUnitApi
34 | @ExperimentalFoundationApi
35 | @ExperimentalCoilApi
36 | @ExperimentalMaterialApi
37 | fun LazyListScope.suggestedSection(data: List?, onEvent: (HomeEvent) -> Unit) {
38 | if (data != null) item(key = "suggested_section") {
39 | SuggestedSection(data) { route ->
40 | onEvent(HomeEvent.Navigate(route))
41 | }
42 | }
43 | }
44 |
45 | @ExperimentalUnitApi
46 | @ExperimentalAnimationApi
47 | @ExperimentalFoundationApi
48 | @ExperimentalCoilApi
49 | @ExperimentalMaterialApi
50 | @Composable
51 | private fun LazyItemScope.SuggestedSection(photos: List, onClick: (Direction) -> Unit) {
52 | Column(
53 | modifier = Modifier.animateItemPlacement(),
54 | verticalArrangement = Arrangement.spacedBy(8.dp)
55 | ) {
56 | SectionTitle(
57 | title = "Suggested for you",
58 | modifier = Modifier.padding(start = 16.dp, top = 8.dp)
59 | )
60 | LazyRow(
61 | horizontalArrangement = Arrangement.spacedBy(8.dp),
62 | contentPadding = PaddingValues(horizontal = 16.dp)
63 | ) {
64 | items(count = photos.size, key = { it }) {
65 | SingleSuggestedItem(photos[it], onClick)
66 | }
67 | }
68 | }
69 | }
70 |
71 | @ExperimentalAnimationApi
72 | @ExperimentalUnitApi
73 | @ExperimentalFoundationApi
74 | @ExperimentalCoilApi
75 | @ExperimentalMaterialApi
76 | @Composable
77 | private fun SingleSuggestedItem(data: UnsplashPhoto, onClick: (Direction) -> Unit) {
78 | var isClicked by remember { mutableStateOf(false) }
79 | val scaleAnim by animateFloatAsState(
80 | targetValue = if (isClicked) 1.05F else 1F,
81 | animationSpec = spring(
82 | stiffness = Spring.StiffnessLow,
83 | dampingRatio = Spring.DampingRatioLowBouncy
84 | ),
85 | )
86 | Surface(
87 | modifier = Modifier
88 | .size(width = 150.dp, height = 250.dp)
89 | .scale(scaleAnim),
90 | onClick = {
91 | isClicked = isClicked.not()
92 | val encodedUrl = URLEncoder.encode(data.smallImage, StandardCharsets.UTF_8.toString())
93 | onClick(DetailScreenDestination(id = data.id, thumbnail = encodedUrl))
94 | },
95 | elevation = 2.dp,
96 | color = Color(android.graphics.Color.parseColor(data.color)),
97 | shape = RoundedCornerShape(10.dp)
98 | ) {
99 | val painter = rememberImagePainter(data = data.smallImage) {
100 | crossfade(350)
101 | }
102 |
103 | Image(
104 | painter = painter,
105 | contentScale = ContentScale.Crop,
106 | contentDescription = data.userDetail.name
107 | )
108 | }
109 | }
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | id 'kotlin-kapt'
5 | id 'dagger.hilt.android.plugin'
6 | id 'org.jetbrains.kotlin.plugin.serialization'
7 | id 'com.google.devtools.ksp' version '1.6.0-1.0.2'
8 | }
9 |
10 | kotlin {
11 | sourceSets {
12 | debug {
13 | kotlin.srcDir("build/generated/ksp/debug/kotlin")
14 | }
15 | release {
16 | kotlin.srcDir("build/generated/ksp/release/kotlin")
17 | }
18 | }
19 | }
20 |
21 | def localProperties = new Properties()
22 | localProperties.load(new FileInputStream(rootProject.file("local.properties")))
23 |
24 | android {
25 | compileSdk 31
26 |
27 | defaultConfig {
28 | applicationId "com.eneskayiklik.wallup"
29 | minSdk 24
30 | targetSdk 31
31 | versionCode 1
32 | versionName "1.0"
33 |
34 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
35 | vectorDrawables {
36 | useSupportLibrary true
37 | }
38 |
39 | buildConfigField "String", "API_KEY", localProperties['apiKey']
40 | }
41 |
42 | buildTypes {
43 | release {
44 | minifyEnabled false
45 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
46 | }
47 | }
48 | compileOptions {
49 | sourceCompatibility JavaVersion.VERSION_1_8
50 | targetCompatibility JavaVersion.VERSION_1_8
51 | }
52 | kotlinOptions {
53 | jvmTarget = '1.8'
54 | useIR = true
55 | }
56 | buildFeatures {
57 | compose true
58 | }
59 | composeOptions {
60 | kotlinCompilerExtensionVersion compose_version
61 | kotlinCompilerVersion '1.6.0'
62 | }
63 | packagingOptions {
64 | resources {
65 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
66 | }
67 | }
68 | }
69 |
70 | dependencies {
71 |
72 | implementation 'androidx.core:core-ktx:1.7.0'
73 | implementation 'androidx.appcompat:appcompat:1.4.0'
74 | implementation 'com.google.android.material:material:1.4.0'
75 | implementation "androidx.compose.ui:ui:$compose_version"
76 | implementation "androidx.compose.material:material:$compose_version"
77 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
78 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
79 | implementation 'androidx.activity:activity-compose:1.4.0'
80 | testImplementation 'junit:junit:4.+'
81 | testImplementation("com.google.truth:truth:1.1.3")
82 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
83 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
84 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
85 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
86 |
87 | // Hilt
88 | implementation "com.google.dagger:hilt-android:2.38.1"
89 | kapt "com.google.dagger:hilt-compiler:2.38.1"
90 | implementation "androidx.hilt:hilt-navigation-compose:1.0.0-beta01"
91 |
92 | // Nav Host
93 | implementation "com.google.accompanist:accompanist-navigation-animation:0.21.4-beta"
94 |
95 | // Insets
96 | implementation "com.google.accompanist:accompanist-insets:0.21.4-beta"
97 |
98 | // Ktor
99 | implementation("io.ktor:ktor-client-android:$ktor_version")
100 | implementation("io.ktor:ktor-client-core:$ktor_version")
101 | implementation("io.ktor:ktor-client-logging:$ktor_version")
102 | implementation("io.ktor:ktor-client-serialization:$ktor_version")
103 |
104 | // Kotlin serialization
105 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version")
106 |
107 | // Coil
108 | implementation("io.coil-kt:coil-compose:1.4.0")
109 |
110 | // Extended material icons
111 | implementation "androidx.compose.material:material-icons-extended:$compose_version"
112 |
113 | // Lottie
114 | implementation "com.airbnb.android:lottie-compose:4.2.2"
115 |
116 | // Room
117 | implementation "androidx.room:room-runtime:$room_version"
118 | annotationProcessor "androidx.room:room-compiler:$room_version"
119 | kapt "androidx.room:room-compiler:$room_version"
120 |
121 | implementation 'io.github.raamcosta.compose-destinations:animations-core:1.1.3-beta'
122 | ksp "io.github.raamcosta.compose-destinations:ksp:1.1.3-beta"
123 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_home/data/repository/HomeRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_home.data.repository
2 |
3 | import com.eneskayiklik.wallup.BuildConfig
4 | import com.eneskayiklik.wallup.R
5 | import com.eneskayiklik.wallup.feature_home.data.dto.UnsplashPhotoDto
6 | import com.eneskayiklik.wallup.feature_home.domain.model.Category
7 | import com.eneskayiklik.wallup.feature_home.domain.model.ColorItem
8 | import com.eneskayiklik.wallup.feature_home.domain.repository.HomeRepository
9 | import com.eneskayiklik.wallup.utils.network.HttpParam
10 | import com.eneskayiklik.wallup.utils.network.HttpRoutes
11 | import com.eneskayiklik.wallup.utils.network.Resource
12 | import io.ktor.client.*
13 | import io.ktor.client.features.*
14 | import io.ktor.client.request.*
15 | import javax.inject.Inject
16 |
17 | class HomeRepositoryImpl @Inject constructor(
18 | private val client: HttpClient
19 | ): HomeRepository {
20 |
21 | override suspend fun getRandomPhotos(): Resource> {
22 | return try {
23 | val data: List = client.get {
24 | url(HttpRoutes.RANDOM_PHOTO)
25 | parameter(HttpParam.CLIENT_ID, BuildConfig.API_KEY)
26 | parameter(HttpParam.COUNT, 10)
27 | }
28 | Resource.Success(data)
29 | } catch (e: HttpRequestTimeoutException) {
30 | Resource.Error("Timeout")
31 | } catch (e: SendCountExceedException) {
32 | Resource.Error("Too Many Request")
33 | } catch (e: Exception) {
34 | Resource.Error("An error occurred")
35 | }
36 | }
37 |
38 | override suspend fun getColorList(): List {
39 | return listOf(
40 | ColorItem("#FEB6B7", "Pink"),
41 | ColorItem("#F6F1F2", "White"),
42 | ColorItem("#6142E0", "Purple"),
43 | ColorItem("#34568B", "Classic Blue"),
44 | ColorItem("#FF6F61", "Living Coral"),
45 | ColorItem("#6B5B95", "Ultra Violet"),
46 | ColorItem("#88B04B", "Greenery"),
47 | ColorItem("#F7CAC9", "Rose Quartz"),
48 | ColorItem("#92A8D1", "Serenity"),
49 | ColorItem("#955251", "Marsala"),
50 | ColorItem("#B565A7", "Radiand Orchid"),
51 | ColorItem("#009B77", "Emerald"),
52 | ColorItem("#DD4124", "Tangerine Tango"),
53 | ColorItem("#D65076", "Honeysucle"),
54 | ColorItem("#45B8AC", "Turquoise"),
55 | ColorItem("#EFC050", "Mimosa"),
56 | ColorItem("#5B5EA6", "Blue Izis"),
57 | ColorItem("#9B2335", "Chili pepper"),
58 | ColorItem("#DFCFBE", "Sand Dollar"),
59 | ColorItem("#55B4B0", "Blue Turquoise"),
60 | ColorItem("#E15D44", "Tigerlily"),
61 | ColorItem("#7FCDCD", "Aqua Sky"),
62 | ColorItem("#BC243C", "True Red"),
63 | ColorItem("#C3447A", "Fuchsia Rose"),
64 | ColorItem("#98B4D4", "Cerulean Blue"),
65 | )
66 | }
67 |
68 | override suspend fun getCategoryList(): List {
69 | return listOf(
70 | Category(title = "Abstract", imageRes = R.drawable.ic_abstract),
71 | Category(title = "Animals", imageRes = R.drawable.ic_animals),
72 | Category(title = "Anime", imageRes = R.drawable.ic_anime),
73 | Category(title = "Art", imageRes = R.drawable.ic_arts),
74 | Category(title = "Cars", imageRes = R.drawable.ic_cars),
75 | Category(title = "City", imageRes = R.drawable.ic_city),
76 | Category(title = "Dark", imageRes = R.drawable.ic_dark),
77 | Category(title = "Flowers", imageRes = R.drawable.ic_flowers),
78 | Category(title = "Food", imageRes = R.drawable.ic_food),
79 | Category(title = "Holidays", imageRes = R.drawable.ic_holidays),
80 | Category(title = "Love", imageRes = R.drawable.ic_love),
81 | Category(title = "Macro", imageRes = R.drawable.ic_macro),
82 | Category(title = "Motorcycles", imageRes = R.drawable.ic_motorcycles),
83 | Category(title = "Music", imageRes = R.drawable.ic_music),
84 | Category(title = "Nature", imageRes = R.drawable.ic_nature),
85 | Category(title = "Space", imageRes = R.drawable.ic_space),
86 | Category(title = "Sport", imageRes = R.drawable.ic_sports),
87 | Category(title = "Technologies", imageRes = R.drawable.ic_tech),
88 | Category(title = "Vector", imageRes = R.drawable.ic_vector),
89 | Category(title = "Words", imageRes = R.drawable.ic_words),
90 | )
91 | }
92 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/di/HomeModule.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.di
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import com.eneskayiklik.wallup.feature_collection.data.repository.CollectionRepositoryImpl
6 | import com.eneskayiklik.wallup.feature_bookmark.data.repository.BookmarkRepositoryImpl
7 | import com.eneskayiklik.wallup.feature_collection.domain.repository.CollectionRepository
8 | import com.eneskayiklik.wallup.feature_bookmark.domain.repository.BookmarkRepository
9 | import com.eneskayiklik.wallup.feature_collection.domain.use_case.CollectionUseCase
10 | import com.eneskayiklik.wallup.feature_bookmark.domain.use_case.BookmarkUseCase
11 | import com.eneskayiklik.wallup.feature_collection.data.repository.SearchRepositoryImpl
12 | import com.eneskayiklik.wallup.feature_collection.domain.repository.SearchRepository
13 | import com.eneskayiklik.wallup.feature_collection.domain.use_case.SearchUseCase
14 | import com.eneskayiklik.wallup.feature_bookmark.data.db.BookmarkDatabase
15 | import com.eneskayiklik.wallup.feature_detail.data.repository.DetailRepositoryImpl
16 | import com.eneskayiklik.wallup.feature_bookmark.data.db.dao.BookmarkPhotoDao
17 | import com.eneskayiklik.wallup.feature_detail.domain.repository.DetailRepository
18 | import com.eneskayiklik.wallup.feature_detail.domain.use_case.DetailUseCase
19 | import com.eneskayiklik.wallup.feature_home.data.repository.HomeRepositoryImpl
20 | import com.eneskayiklik.wallup.feature_home.domain.repository.HomeRepository
21 | import com.eneskayiklik.wallup.feature_home.domain.use_case.HomeUseCase
22 | import dagger.Module
23 | import dagger.Provides
24 | import dagger.hilt.InstallIn
25 | import dagger.hilt.android.qualifiers.ApplicationContext
26 | import dagger.hilt.components.SingletonComponent
27 | import io.ktor.client.*
28 | import io.ktor.client.engine.android.*
29 | import io.ktor.client.features.json.*
30 | import io.ktor.client.features.json.serializer.*
31 | import io.ktor.client.features.logging.*
32 | import javax.inject.Singleton
33 |
34 | @Module
35 | @InstallIn(SingletonComponent::class)
36 | object HomeModule {
37 |
38 | @Provides
39 | @Singleton
40 | fun provideHttpClient() = HttpClient(Android) {
41 | install(Logging) {
42 | level = LogLevel.ALL
43 | }
44 | install(JsonFeature) {
45 | serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
46 | coerceInputValues = true
47 | ignoreUnknownKeys = true
48 | })
49 | }
50 | }
51 |
52 | @Singleton
53 | @Provides
54 | fun provideRoomDb(@ApplicationContext context: Context) =
55 | Room.databaseBuilder(context, BookmarkDatabase::class.java, "bookmark").build()
56 |
57 | @Singleton
58 | @Provides
59 | fun provideBookmarkDao(db: BookmarkDatabase): BookmarkPhotoDao =
60 | db.bookmarkDao()
61 |
62 | @Singleton
63 | @Provides
64 | fun provideHomeRepository(
65 | client: HttpClient
66 | ): HomeRepository = HomeRepositoryImpl(client)
67 |
68 | @Singleton
69 | @Provides
70 | fun provideHomeUseCase(
71 | repository: HomeRepository
72 | ): HomeUseCase = HomeUseCase(repository)
73 |
74 | @Singleton
75 | @Provides
76 | fun provideDetailRepository(
77 | client: HttpClient,
78 | dao: BookmarkPhotoDao
79 | ): DetailRepository = DetailRepositoryImpl(client, dao)
80 |
81 | @Singleton
82 | @Provides
83 | fun provideDetailUseCase(
84 | repository: DetailRepository
85 | ): DetailUseCase = DetailUseCase(repository)
86 |
87 | @Singleton
88 | @Provides
89 | fun provideCollectionRepository(
90 | client: HttpClient
91 | ): CollectionRepository = CollectionRepositoryImpl(client)
92 |
93 | @Singleton
94 | @Provides
95 | fun provideCollectionUseCase(
96 | repository: CollectionRepository
97 | ): CollectionUseCase = CollectionUseCase(repository)
98 |
99 | @Singleton
100 | @Provides
101 | fun provideSearchRepository(
102 | client: HttpClient
103 | ): SearchRepository = SearchRepositoryImpl(client)
104 |
105 | @Singleton
106 | @Provides
107 | fun provideSearchUseCase(
108 | repository: SearchRepository
109 | ): SearchUseCase = SearchUseCase(repository)
110 |
111 | @Singleton
112 | @Provides
113 | fun provideBookmarkRepository(
114 | dao: BookmarkPhotoDao
115 | ): BookmarkRepository = BookmarkRepositoryImpl(dao)
116 |
117 | @Singleton
118 | @Provides
119 | fun provideBookmarkUseCase(
120 | repository: BookmarkRepository
121 | ): BookmarkUseCase = BookmarkUseCase(repository)
122 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # WallUp 🏞️
4 |
5 | Wallpaper finder and downloader app Demonstrate the Jetpack Compose UI
6 | using [Unsplash](https://unsplash.com/developers) API *Made with ❤️
7 | by [Enes](https://github.com/Enes-Kayiklik)*
8 |
9 |
10 |
11 | ## UI Design 🎨
12 |
13 | ***Thanks to [Rian Hamidjoyo](https://dribbble.com/rseth)
14 | for [Wallpaper App UI Design](https://dribbble.com/shots/14808564-Wallpaper-app)***
15 |
16 | ## Screens 🖼
17 |
18 |
19 |
20 | | Home Screen |
21 | Detail Screen |
22 |
23 |
24 |  |
25 |  |
26 |
27 |
28 |
29 |
30 |
31 | | Bookmark Screen |
32 | Collection Screen |
33 |
34 |
35 |  |
36 |  |
37 |
38 |
39 |
40 | ## Prerequisites
41 |
42 | - #### API Key
43 |
44 | To run the application, an API key from [Unsplash](https://unsplash.com/developers) should be
45 | supplied.
46 |
47 | inside **local.properties** file add this line and Rebuild project.
48 | `` apiKey="Your API Key Here" ``
49 |
50 | How to store API key? - [Stackoverflow](https://stackoverflow.com/a/70244128/13447094)
51 |
52 | ## Architecture 🗼
53 |
54 | - Single Activity No Fragment
55 | - MVVM Pattern
56 |
57 | **View:** Renders UI and delegates user actions to ViewModel
58 |
59 | **ViewModel:** Can have simple UI logic but most of the time just gets the data from UseCase.
60 |
61 | **UseCase:** Contains all business rules and they written in the manner of single responsibility
62 | principle.
63 |
64 | **Repository:** Single source of data. Responsible to get data from one or more data sources.
65 |
66 | **For more information you can
67 | check [Guide to app architecture](https://developer.android.com/jetpack/guide?gclid=CjwKCAiA_omPBhBBEiwAcg7smXcfbEYneoLKFD_4Tyw0OgVQkpZL_XIr5TPXT0mncuQhgDIBBvLhbBoCEx0QAvD_BwE&gclsrc=aw.ds#mobile-app-ux)**
68 |
69 |
70 |
71 | ## Libraries 📚
72 |
73 | - [Kotlin](https://kotlinlang.org/) - First class and official programming language for Android
74 | development.
75 | - [Ktor Client](https://ktor.io/docs/client.html) - Ktor includes a multiplatform asynchronous HTTP
76 | client, which allows you to make requests and handle responses, extend its functionality with
77 | plugins (formerly known as features), such as authentication, JSON serialization, and so on. In
78 | this topic, we'll take an overview of the client - from setting it up to making requests and
79 | installing plugins.
80 | - [Jetpack Compose](https://developer.android.com/jetpack/compose) - Jetpack Compose is Android’s
81 | modern toolkit for building native UI.
82 | - [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) - For asynchronous
83 | and more..
84 | - [Android Architecture Components](https://developer.android.com/topic/libraries/architecture) -
85 | Collection of libraries that help you design robust, testable, and maintainable apps.
86 | - [Flows](https://developer.android.com/kotlin/flow) - Data objects that notify views when the
87 | underlying database changes.
88 | - [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) - Stores
89 | UI-related data that isn't destroyed on UI changes.
90 | - [Room](https://developer.android.com/topic/libraries/architecture/room) - Database Library
91 | - [Compose Destinations](https://github.com/raamcosta/compose-destinations) - A KSP library that
92 | processes annotations and generates code that uses Official Jetpack Compose Navigation under the
93 | hood. It hides from you the non-type-safe and boilerplate code you would otherwise have to write.
94 | - [Material Components for Android](https://github.com/material-components/material-components-android)
95 | - Modular and customizable Material Design UI components for Android.
96 | - [Dagger - Hilt](https://dagger.dev/hilt/) - Dependency Injection Framework
97 | - [Coil](https://coil-kt.github.io/coil/compose/) - Image loader library.
98 |
99 | ## Package Structure 🗂
100 |
101 | .
102 | .
103 | .
104 | ├── di # Hilt Dependency Injection
105 | ├── feature_bookmark
106 | ├── feature_collection
107 | ├── feature_detail
108 | ├── feature_home
109 | ├── feature_splash
110 | | ├── data # DTOs and repositories implementation
111 | | |
112 | | ├── domain # Models, repositories and use cases
113 | | |
114 | | └── presentation # UI Components
115 | ├── ui
116 | | ├── theme # Compose Theme
117 | | |
118 | | └── animation # Animation Utils
119 | |
120 | ├── utils # Useful classes
121 | |
122 | └── WallUpApp.kt # @HiltAndroidApp
123 |
124 | ## Contribute 🤝
125 |
126 | If you want to contribute to this app, you're always welcome!
127 |
128 | ## License 📄
129 |
130 | ```
131 | Copyright 2022 Enes-Kayiklik
132 |
133 | Licensed under the Apache License, Version 2.0 (the "License");
134 | you may not use this file except in compliance with the License.
135 | You may obtain a copy of the License at
136 |
137 | http://www.apache.org/licenses/LICENSE-2.0
138 |
139 | Unless required by applicable law or agreed to in writing, software
140 | distributed under the License is distributed on an "AS IS" BASIS,
141 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
142 | See the License for the specific language governing permissions and
143 | limitations under the License.
144 | ```
145 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_detail/presentation/component/DetailButtonStack.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_detail.presentation.component
2 |
3 | import android.graphics.drawable.Drawable
4 | import androidx.compose.animation.AnimatedVisibility
5 | import androidx.compose.animation.ExperimentalAnimationApi
6 | import androidx.compose.animation.core.Spring
7 | import androidx.compose.animation.core.animateFloatAsState
8 | import androidx.compose.animation.core.spring
9 | import androidx.compose.animation.scaleIn
10 | import androidx.compose.foundation.Image
11 | import androidx.compose.foundation.layout.*
12 | import androidx.compose.foundation.shape.RoundedCornerShape
13 | import androidx.compose.material.*
14 | import androidx.compose.material.icons.Icons
15 | import androidx.compose.material.icons.filled.*
16 | import androidx.compose.runtime.*
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.draw.rotate
20 | import androidx.compose.ui.draw.scale
21 | import androidx.compose.ui.graphics.Color
22 | import androidx.compose.ui.graphics.ColorFilter
23 | import androidx.compose.ui.graphics.vector.ImageVector
24 | import androidx.compose.ui.platform.LocalContext
25 | import androidx.compose.ui.unit.dp
26 | import com.eneskayiklik.wallup.feature_detail.domain.model.DetailEvent
27 |
28 | @ExperimentalAnimationApi
29 | @ExperimentalMaterialApi
30 | @Composable
31 | fun DetailButtonStack(
32 | modifier: Modifier = Modifier,
33 | isVisible: Boolean,
34 | isBookmarked: Boolean,
35 | isDownloading: Boolean,
36 | imageBitmap: Drawable?,
37 | imageUrl: String,
38 | thumbnailUrl: String,
39 | imageId: String,
40 | createdAt: String,
41 | buttonsBackColor: String?,
42 | onEvent: (DetailEvent) -> Unit
43 | ) {
44 | AnimatedVisibility(
45 | modifier = modifier,
46 | visible = isVisible,
47 | enter = scaleIn(
48 | spring(
49 | dampingRatio = Spring.DampingRatioMediumBouncy,
50 | stiffness = Spring.StiffnessLow
51 | )
52 | )
53 | ) {
54 | Row(
55 | modifier = Modifier.fillMaxWidth(),
56 | horizontalArrangement = Arrangement.SpaceEvenly
57 | ) {
58 | BookmarkButtonContainer(
59 | color = buttonsBackColor,
60 | isBookmarked = isBookmarked,
61 | thumbnailUrl = thumbnailUrl,
62 | id = imageId,
63 | onEvent = onEvent
64 | )
65 | DownloadButtonContainer(
66 | color = buttonsBackColor,
67 | isDownloading = isDownloading,
68 | imageUrl = imageUrl,
69 | createdAt = createdAt,
70 | onEvent = onEvent
71 | )
72 | BrushButtonContainer(
73 | color = buttonsBackColor,
74 | imageBitmap = imageBitmap,
75 | onEvent = onEvent
76 | )
77 | }
78 | }
79 | }
80 |
81 | @ExperimentalMaterialApi
82 | @Composable
83 | private fun BookmarkButtonContainer(
84 | color: String?,
85 | isBookmarked: Boolean,
86 | id: String,
87 | thumbnailUrl: String,
88 | onEvent: (DetailEvent) -> Unit
89 | ) {
90 | val animateRotation by animateFloatAsState(
91 | targetValue = if (isBookmarked) 360F else 0F,
92 | animationSpec = spring()
93 | )
94 | val icon = if (isBookmarked.not()) Icons.Default.BookmarkAdd else Icons.Default.BookmarkRemove
95 | SingleButton(icon = icon, modifier = Modifier.rotate(animateRotation), color = color) {
96 | onEvent(DetailEvent.OnBookmarkClick(id, thumbnailUrl, color ?: "", isBookmarked.not()))
97 | }
98 | }
99 |
100 | @ExperimentalMaterialApi
101 | @Composable
102 | private fun DownloadButtonContainer(
103 | color: String?, isDownloading: Boolean, imageUrl: String,
104 | createdAt: String, onEvent: (DetailEvent) -> Unit
105 | ) {
106 | val scaleAnim by animateFloatAsState(targetValue = if (isDownloading) 1F else 0F)
107 | Box(contentAlignment = Alignment.Center) {
108 | CircularProgressIndicator(
109 | modifier = Modifier
110 | .size(50.dp)
111 | .scale(scaleAnim),
112 | color = Color.White
113 | )
114 | SingleButton(icon = Icons.Default.Download, color = color) {
115 | if (isDownloading.not())
116 | onEvent(DetailEvent.OnDownloadClick(imageUrl, createdAt))
117 | }
118 | }
119 | }
120 |
121 | @ExperimentalMaterialApi
122 | @Composable
123 | private fun BrushButtonContainer(
124 | color: String?,
125 | imageBitmap: Drawable?,
126 | onEvent: (DetailEvent) -> Unit
127 | ) {
128 | val context = LocalContext.current
129 | Column(horizontalAlignment = Alignment.CenterHorizontally) {
130 | SingleButton(
131 | icon = Icons.Default.Brush,
132 | color = color
133 | ) { onEvent(
134 | DetailEvent.OnWallpaper(
135 | context,
136 | imageBitmap
137 | )
138 | ) }
139 | }
140 | }
141 |
142 | @ExperimentalMaterialApi
143 | @Composable
144 | private fun SingleButton(
145 | icon: ImageVector,
146 | color: String?,
147 | modifier: Modifier = Modifier,
148 | onClick: () -> Unit
149 | ) {
150 | val mColor = if (color.isNullOrEmpty()
151 | .not()
152 | ) Color(android.graphics.Color.parseColor(color)).copy(alpha = 0.5F) else MaterialTheme.colors.background
153 | Surface(
154 | modifier = Modifier.size(50.dp),
155 | shape = RoundedCornerShape(10.dp),
156 | color = mColor,
157 | onClick = onClick
158 | ) {
159 | Box(modifier = Modifier.fillMaxSize()) {
160 | Image(
161 | modifier = modifier
162 | .size(25.dp)
163 | .align(Alignment.Center),
164 | imageVector = icon,
165 | colorFilter = ColorFilter.tint(Color.White),
166 | contentDescription = icon.name
167 | )
168 | }
169 | }
170 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/utils/blur_hash/BlurHashDecoder.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.utils.blur_hash
2 |
3 | import android.graphics.Bitmap
4 | import android.graphics.Color
5 | import kotlin.math.cos
6 | import kotlin.math.pow
7 | import kotlin.math.withSign
8 |
9 | object BlurHashDecoder {
10 |
11 | // cache Math.cos() calculations to improve performance.
12 | // The number of calculations can be huge for many bitmaps: width * height * numCompX * numCompY * 2 * nBitmaps
13 | // the cache is enabled by default, it is recommended to disable it only when just a few images are displayed
14 | private val cacheCosinesX = HashMap()
15 | private val cacheCosinesY = HashMap()
16 |
17 | /**
18 | * Clear calculations stored in memory cache.
19 | * The cache is not big, but will increase when many image sizes are used,
20 | * if the app needs memory it is recommended to clear it.
21 | */
22 | fun clearCache() {
23 | cacheCosinesX.clear()
24 | cacheCosinesY.clear()
25 | }
26 |
27 | /**
28 | * Decode a blur hash into a new bitmap.
29 | *
30 | * @param useCache use in memory cache for the calculated math, reused by images with same size.
31 | * if the cache does not exist yet it will be created and populated with new calculations.
32 | * By default it is true.
33 | */
34 | fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f, useCache: Boolean = true): Bitmap? {
35 | if (blurHash == null || blurHash.length < 6) {
36 | return null
37 | }
38 | val numCompEnc = decode83(blurHash, 0, 1)
39 | val numCompX = (numCompEnc % 9) + 1
40 | val numCompY = (numCompEnc / 9) + 1
41 | if (blurHash.length != 4 + 2 * numCompX * numCompY) {
42 | return null
43 | }
44 | val maxAcEnc = decode83(blurHash, 1, 2)
45 | val maxAc = (maxAcEnc + 1) / 166f
46 | val colors = Array(numCompX * numCompY) { i ->
47 | if (i == 0) {
48 | val colorEnc = decode83(blurHash, 2, 6)
49 | decodeDc(colorEnc)
50 | } else {
51 | val from = 4 + i * 2
52 | val colorEnc = decode83(blurHash, from, from + 2)
53 | decodeAc(colorEnc, maxAc * punch)
54 | }
55 | }
56 | return composeBitmap(width, height, numCompX, numCompY, colors, useCache)
57 | }
58 |
59 | private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int {
60 | var result = 0
61 | for (i in from until to) {
62 | val index = charMap[str[i]] ?: -1
63 | if (index != -1) {
64 | result = result * 83 + index
65 | }
66 | }
67 | return result
68 | }
69 |
70 | private fun decodeDc(colorEnc: Int): FloatArray {
71 | val r = colorEnc shr 16
72 | val g = (colorEnc shr 8) and 255
73 | val b = colorEnc and 255
74 | return floatArrayOf(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b))
75 | }
76 |
77 | private fun srgbToLinear(colorEnc: Int): Float {
78 | val v = colorEnc / 255f
79 | return if (v <= 0.04045f) {
80 | (v / 12.92f)
81 | } else {
82 | ((v + 0.055f) / 1.055f).pow(2.4f)
83 | }
84 | }
85 |
86 | private fun decodeAc(value: Int, maxAc: Float): FloatArray {
87 | val r = value / (19 * 19)
88 | val g = (value / 19) % 19
89 | val b = value % 19
90 | return floatArrayOf(
91 | signedPow2((r - 9) / 9.0f) * maxAc,
92 | signedPow2((g - 9) / 9.0f) * maxAc,
93 | signedPow2((b - 9) / 9.0f) * maxAc
94 | )
95 | }
96 |
97 | private fun signedPow2(value: Float) = value.pow(2f).withSign(value)
98 |
99 | private fun composeBitmap(
100 | width: Int, height: Int,
101 | numCompX: Int, numCompY: Int,
102 | colors: Array,
103 | useCache: Boolean
104 | ): Bitmap {
105 | // use an array for better performance when writing pixel colors
106 | val imageArray = IntArray(width * height)
107 | val calculateCosX = !useCache || !cacheCosinesX.containsKey(width * numCompX)
108 | val cosinesX = getArrayForCosinesX(calculateCosX, width, numCompX)
109 | val calculateCosY = !useCache || !cacheCosinesY.containsKey(height * numCompY)
110 | val cosinesY = getArrayForCosinesY(calculateCosY, height, numCompY)
111 | for (y in 0 until height) {
112 | for (x in 0 until width) {
113 | var r = 0f
114 | var g = 0f
115 | var b = 0f
116 | for (j in 0 until numCompY) {
117 | for (i in 0 until numCompX) {
118 | val cosX = cosinesX.getCos(calculateCosX, i, numCompX, x, width)
119 | val cosY = cosinesY.getCos(calculateCosY, j, numCompY, y, height)
120 | val basis = (cosX * cosY).toFloat()
121 | val color = colors[j * numCompX + i]
122 | r += color[0] * basis
123 | g += color[1] * basis
124 | b += color[2] * basis
125 | }
126 | }
127 | imageArray[x + width * y] = Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b))
128 | }
129 | }
130 | return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888)
131 | }
132 |
133 | private fun getArrayForCosinesY(calculate: Boolean, height: Int, numCompY: Int) = when {
134 | calculate -> {
135 | DoubleArray(height * numCompY).also {
136 | cacheCosinesY[height * numCompY] = it
137 | }
138 | }
139 | else -> {
140 | cacheCosinesY[height * numCompY]!!
141 | }
142 | }
143 |
144 | private fun getArrayForCosinesX(calculate: Boolean, width: Int, numCompX: Int) = when {
145 | calculate -> {
146 | DoubleArray(width * numCompX).also {
147 | cacheCosinesX[width * numCompX] = it
148 | }
149 | }
150 | else -> cacheCosinesX[width * numCompX]!!
151 | }
152 |
153 | private fun DoubleArray.getCos(
154 | calculate: Boolean,
155 | x: Int,
156 | numComp: Int,
157 | y: Int,
158 | size: Int
159 | ): Double {
160 | if (calculate) {
161 | this[x + numComp * y] = cos(Math.PI * y * x / size)
162 | }
163 | return this[x + numComp * y]
164 | }
165 |
166 | private fun linearToSrgb(value: Float): Int {
167 | val v = value.coerceIn(0f, 1f)
168 | return if (v <= 0.0031308f) {
169 | (v * 12.92f * 255f + 0.5f).toInt()
170 | } else {
171 | ((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt()
172 | }
173 | }
174 |
175 | private val charMap = listOf(
176 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
177 | 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
178 | 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
179 | 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',',
180 | '-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~'
181 | )
182 | .mapIndexed { i, c -> c to i }
183 | .toMap()
184 |
185 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_detail/presentation/DetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_detail.presentation
2 |
3 | import android.app.DownloadManager
4 | import android.app.WallpaperManager
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.graphics.drawable.BitmapDrawable
8 | import android.graphics.drawable.Drawable
9 | import android.net.Uri
10 | import android.util.Log
11 | import androidx.compose.animation.ExperimentalAnimationApi
12 | import androidx.compose.foundation.ExperimentalFoundationApi
13 | import androidx.compose.material.ExperimentalMaterialApi
14 | import androidx.compose.ui.unit.ExperimentalUnitApi
15 | import androidx.lifecycle.SavedStateHandle
16 | import androidx.lifecycle.ViewModel
17 | import androidx.lifecycle.viewModelScope
18 | import coil.annotation.ExperimentalCoilApi
19 | import com.eneskayiklik.wallup.destinations.CollectionScreenDestination
20 | import com.eneskayiklik.wallup.feature_bookmark.data.db.entity.BookmarkPhoto
21 | import com.eneskayiklik.wallup.feature_detail.domain.model.DetailEvent
22 | import com.eneskayiklik.wallup.feature_detail.domain.model.DetailState
23 | import com.eneskayiklik.wallup.feature_detail.domain.model.DownloadType
24 | import com.eneskayiklik.wallup.feature_detail.domain.model.ScreenType.*
25 | import com.eneskayiklik.wallup.feature_detail.domain.use_case.DetailUseCase
26 | import com.eneskayiklik.wallup.utils.extensions.getImageUri
27 | import com.eneskayiklik.wallup.utils.model.UiEvent
28 | import com.eneskayiklik.wallup.utils.network.Resource
29 | import dagger.hilt.android.lifecycle.HiltViewModel
30 | import kotlinx.coroutines.Dispatchers
31 | import kotlinx.coroutines.flow.*
32 | import kotlinx.coroutines.launch
33 | import java.net.URLDecoder
34 | import java.nio.charset.StandardCharsets
35 | import javax.inject.Inject
36 |
37 | @HiltViewModel
38 | class DetailViewModel @Inject constructor(
39 | private val useCase: DetailUseCase,
40 | private val downloadManager: DownloadManager,
41 | private val wallpaperManager: WallpaperManager,
42 | args: SavedStateHandle
43 | ) : ViewModel() {
44 | private val _detailState = MutableStateFlow(DetailState())
45 | val detailState: StateFlow = _detailState
46 |
47 | private val _uiEvent = MutableSharedFlow()
48 | val uiEvent: SharedFlow = _uiEvent
49 |
50 | private var _downloadType = DownloadType.NONE
51 | var mDownloadId: Long? = null
52 |
53 | init {
54 | getPhotoDetail(
55 | args.get("id"),
56 | args.get("thumbnail")
57 | )
58 | }
59 |
60 | @ExperimentalCoilApi
61 | @ExperimentalUnitApi
62 | @ExperimentalAnimationApi
63 | @ExperimentalMaterialApi
64 | @ExperimentalFoundationApi
65 | fun onEvent(event: DetailEvent) {
66 | viewModelScope.launch {
67 | when (event) {
68 | is DetailEvent.OnBookmarkClick -> {
69 | updateBookmarkState(event.id, event.thumbnail, event.color, event.addBookmark)
70 | }
71 | is DetailEvent.OnDownloadClick -> startDownload(event.url, event.createdAt)
72 | is DetailEvent.OnWallpaper -> event.context.setAsWallpaper(event.bitmap)
73 | is DetailEvent.Navigate -> {
74 | viewModelScope.launch {
75 | _uiEvent.emit(
76 | if (event.id == null) UiEvent.PopBack
77 | else UiEvent.OnNavigate(CollectionScreenDestination(title = event.title, collectionId = event.id))
78 | )
79 | }
80 | }
81 | is DetailEvent.UpdateDrawable -> {
82 | _detailState.value = _detailState.value.copy(
83 | imageDrawable = event.drawable
84 | )
85 | }
86 | is DetailEvent.Share -> event.context.shareImage(event.drawable)
87 | is DetailEvent.ShareText -> event.context.shareText(event.data)
88 | }
89 | }
90 | }
91 |
92 | private fun updateBookmarkState(
93 | id: String,
94 | thumbnail: String,
95 | color: String,
96 | addBookmark: Boolean
97 | ) {
98 | viewModelScope.launch(Dispatchers.IO) {
99 | _detailState.value = _detailState.value.copy(
100 | imageDetail = _detailState.value.imageDetail?.copy(
101 | isBookmarked = addBookmark
102 | )
103 | )
104 | if (addBookmark) {
105 | useCase.addBookmark(
106 | BookmarkPhoto(
107 | unsplashId = id,
108 | thumbnail = thumbnail,
109 | color = color
110 | )
111 | )
112 | } else {
113 | useCase.removeBookmark(id)
114 | }
115 | }
116 | }
117 |
118 | private fun getPhotoDetail(id: String?, thumbnail: String?) {
119 | viewModelScope.launch(Dispatchers.IO) {
120 | if (thumbnail.isNullOrEmpty().not()) {
121 | val decodedUrl = URLDecoder.decode(thumbnail, StandardCharsets.UTF_8.toString())
122 | _detailState.value = _detailState.value.copy(thumbnail = decodedUrl)
123 | }
124 |
125 | // make api request for getting detail of selected photo
126 | if (id == null) {
127 | // TODO("error state")
128 | return@launch
129 | }
130 | useCase.getPhotoDetail(id).collectLatest {
131 | when (it) {
132 | is Resource.Error -> Log.e("DetailViewModel", it.message)
133 | is Resource.Loading -> {}
134 | is Resource.Success -> {
135 | _detailState.value = _detailState.value.copy(
136 | imageDetail = it.data
137 | )
138 | }
139 | }
140 | }
141 | }
142 | }
143 |
144 | private fun startDownload(url: String, createdAt: String) {
145 | if (url.isNotEmpty()) {
146 | val req = DownloadManager.Request(
147 | Uri.parse(url)
148 | ).setTitle(createdAt)
149 | .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
150 | .setAllowedOverMetered(true)
151 | val downloadId = downloadManager.enqueue(req)
152 | viewModelScope.launch {
153 | _detailState.value = _detailState.value.copy(
154 | currentDownloadId = downloadId
155 | )
156 | mDownloadId = downloadId
157 | _uiEvent.emit(UiEvent.ShowToast("Download started"))
158 | }
159 | }
160 | }
161 |
162 | fun downloadComplete() {
163 | viewModelScope.launch {
164 | if (mDownloadId != null) {
165 | mDownloadId = null
166 | _detailState.value = _detailState.value.copy(
167 | currentDownloadId = null
168 | )
169 | _uiEvent.emit(UiEvent.ShowToast("Download complete"))
170 |
171 | when (_downloadType) {
172 | DownloadType.SHARE -> {
173 |
174 | }
175 | DownloadType.NONE -> {
176 | return@launch
177 | }
178 | }
179 | _downloadType = DownloadType.NONE
180 | }
181 | }
182 | }
183 |
184 | private fun Context.setAsWallpaper(drawable: Drawable?) {
185 | viewModelScope.launch(Dispatchers.IO) {
186 | try {
187 | val bitmap = (drawable as? BitmapDrawable)?.bitmap
188 | startActivity(wallpaperManager.getCropAndSetWallpaperIntent(getImageUri(bitmap)))
189 | } catch (e: Exception) {
190 | e.printStackTrace()
191 | _uiEvent.emit(UiEvent.ShowToast("Unsuccessful"))
192 | }
193 | }
194 | }
195 |
196 | private fun Context.shareImage(drawable: Drawable?) {
197 | viewModelScope.launch(Dispatchers.IO) {
198 | try {
199 | val uri = getImageUri((drawable as? BitmapDrawable)?.bitmap)
200 | Intent(Intent.ACTION_SEND).apply {
201 | type = "image/jpeg"
202 | putExtra(Intent.EXTRA_STREAM, uri)
203 | startActivity(Intent.createChooser(this, "Share via"))
204 | }
205 | } catch (e: Exception) {
206 | e.printStackTrace()
207 | _uiEvent.emit(UiEvent.ShowToast("Try again after the photo is uploaded"))
208 | }
209 | }
210 | }
211 |
212 | private fun Context.shareText(data: String) {
213 | Intent(Intent.ACTION_VIEW).apply {
214 | setData(Uri.parse(data))
215 | startActivity(Intent.createChooser(this, "View via"))
216 | }
217 | }
218 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/app/src/main/java/com/eneskayiklik/wallup/feature_detail/presentation/component/DetailImageInfoItem.kt:
--------------------------------------------------------------------------------
1 | package com.eneskayiklik.wallup.feature_detail.presentation.component
2 |
3 | import android.graphics.drawable.Drawable
4 | import androidx.compose.foundation.Image
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.layout.*
7 | import androidx.compose.foundation.lazy.LazyListScope
8 | import androidx.compose.foundation.shape.CircleShape
9 | import androidx.compose.foundation.shape.RoundedCornerShape
10 | import androidx.compose.material.*
11 | import androidx.compose.material.icons.Icons
12 | import androidx.compose.material.icons.filled.*
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.draw.clip
17 | import androidx.compose.ui.graphics.vector.ImageVector
18 | import androidx.compose.ui.layout.ContentScale
19 | import androidx.compose.ui.platform.LocalContext
20 | import androidx.compose.ui.res.painterResource
21 | import androidx.compose.ui.text.SpanStyle
22 | import androidx.compose.ui.text.buildAnnotatedString
23 | import androidx.compose.ui.text.font.FontWeight
24 | import androidx.compose.ui.text.style.TextAlign
25 | import androidx.compose.ui.text.withStyle
26 | import androidx.compose.ui.unit.*
27 | import coil.annotation.ExperimentalCoilApi
28 | import coil.compose.rememberImagePainter
29 | import com.eneskayiklik.wallup.R
30 | import com.eneskayiklik.wallup.feature_detail.domain.model.DetailEvent
31 | import com.eneskayiklik.wallup.feature_detail.domain.model.DetailState
32 | import com.eneskayiklik.wallup.feature_home.domain.model.PhotoDetail
33 | import com.eneskayiklik.wallup.feature_home.domain.model.UnsplashPhoto
34 | import com.eneskayiklik.wallup.feature_home.domain.model.UserDetail
35 | import com.eneskayiklik.wallup.utils.const.INSTAGRAM_LINK
36 | import com.eneskayiklik.wallup.utils.const.TWITTER_LINK
37 | import com.eneskayiklik.wallup.utils.extensions.parseCount
38 |
39 | @ExperimentalCoilApi
40 | @ExperimentalUnitApi
41 | @ExperimentalMaterialApi
42 | fun LazyListScope.imageInfoItem(state: DetailState, onEvent: (DetailEvent) -> Unit) {
43 | item { ActionButtonGroup(onEvent, state.imageDrawable) }
44 | item { ImageInfoItem(state.imageDetail, onEvent) }
45 | detailRelatedCollection(state.imageDetail?.relatedCollections, onEvent)
46 | /*item { Divider(modifier = Modifier.height(1.dp)) }
47 | item { ExifInfoContainer(imageDetail.photoDetail) }*/
48 | }
49 |
50 | @ExperimentalUnitApi
51 | @ExperimentalMaterialApi
52 | @Composable
53 | private fun ImageInfoItem(imageDetail: UnsplashPhoto?, onEvent: (DetailEvent) -> Unit) {
54 | if (imageDetail == null)
55 | return
56 | Column(
57 | modifier = Modifier
58 | .fillMaxWidth()
59 | .padding(vertical = 8.dp, horizontal = 16.dp),
60 | horizontalAlignment = Alignment.CenterHorizontally,
61 | verticalArrangement = Arrangement.spacedBy(4.dp)
62 | ) {
63 | UserSection(userDetail = imageDetail.userDetail, imageDetail.sourceUrl, onEvent)
64 | PropertySection(imageDetail)
65 | }
66 | }
67 |
68 | @Composable
69 | private fun ActionButtonGroup(onEvent: (DetailEvent) -> Unit, drawable: Drawable?) {
70 | val context = LocalContext.current
71 | Row(
72 | modifier = Modifier
73 | .fillMaxWidth()
74 | .padding(horizontal = 8.dp),
75 | horizontalArrangement = Arrangement.SpaceBetween
76 | ) {
77 | IconButton(onClick = { onEvent(DetailEvent.Navigate(null)) }) {
78 | Icon(
79 | imageVector = Icons.Default.ArrowBack,
80 | modifier = Modifier.size(24.dp),
81 | contentDescription = "Back",
82 | tint = MaterialTheme.colors.onBackground
83 | )
84 | }
85 | IconButton(onClick = { onEvent(DetailEvent.Share(drawable, context)) }) {
86 | Icon(
87 | imageVector = Icons.Default.Share,
88 | modifier = Modifier.size(24.dp),
89 | contentDescription = "Share with..",
90 | tint = MaterialTheme.colors.onBackground
91 | )
92 | }
93 | }
94 | }
95 |
96 | @ExperimentalUnitApi
97 | @ExperimentalMaterialApi
98 | @Composable
99 | private fun UserSection(userDetail: UserDetail, unsplashSource: String, onEvent: (DetailEvent) -> Unit) {
100 | val ppPainter = rememberImagePainter(data = userDetail.image)
101 | val context = LocalContext.current
102 | Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
103 | Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
104 | Image(
105 | painter = ppPainter,
106 | contentDescription = "User Profile Photo",
107 | contentScale = ContentScale.Crop,
108 | modifier = Modifier
109 | .size(64.dp)
110 | .clip(
111 | CircleShape
112 | )
113 | )
114 | Column(
115 | modifier = Modifier.height(64.dp),
116 | verticalArrangement = Arrangement.SpaceAround
117 | ) {
118 | Text(
119 | text = userDetail.name,
120 | style = MaterialTheme.typography.body1,
121 | color = MaterialTheme.colors.onBackground
122 | )
123 | Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
124 | if (userDetail.unsplashProfile.isNotEmpty()) {
125 | IconButton(onClick = {
126 | onEvent(DetailEvent.ShareText(userDetail.unsplashProfile, context))
127 | }, modifier = Modifier.size(24.dp)) {
128 | Image(
129 | painter = painterResource(id = R.drawable.ic_unsplash),
130 | modifier = Modifier.size(24.dp),
131 | contentScale = ContentScale.Crop,
132 | contentDescription = "Twitter"
133 | )
134 | }
135 | }
136 | if (userDetail.instaUserName.isNotEmpty()) {
137 | IconButton(onClick = {
138 | onEvent(
139 | DetailEvent.ShareText(
140 | INSTAGRAM_LINK.plus(
141 | userDetail.instaUserName
142 | ), context
143 | )
144 | )
145 | }, modifier = Modifier.size(24.dp)) {
146 | Image(
147 | painter = painterResource(id = R.drawable.ic_instagram),
148 | modifier = Modifier.size(24.dp),
149 | contentScale = ContentScale.Crop,
150 | contentDescription = "Instagram"
151 | )
152 | }
153 | }
154 | if (userDetail.twitterUserName.isNotEmpty()) {
155 | IconButton(onClick = {
156 | onEvent(
157 | DetailEvent.ShareText(
158 | TWITTER_LINK.plus(
159 | userDetail.twitterUserName
160 | ), context
161 | )
162 | )
163 | }, modifier = Modifier.size(24.dp)) {
164 | Image(
165 | painter = painterResource(id = R.drawable.ic_twitter),
166 | modifier = Modifier.size(24.dp),
167 | contentScale = ContentScale.Crop,
168 | contentDescription = "Twitter"
169 | )
170 | }
171 | }
172 | }
173 | }
174 | }
175 | TextButton(onClick = { onEvent(
176 | DetailEvent.ShareText(
177 | unsplashSource, context
178 | )
179 | ) }) {
180 | val text = buildAnnotatedString {
181 | withStyle(SpanStyle(fontWeight = FontWeight.Bold, fontSize = 14.sp)) {
182 | append("Show source\n")
183 | }
184 | withStyle(SpanStyle(fontWeight = FontWeight.SemiBold, fontSize = 11.sp)) {
185 | append("Unsplash.com")
186 | }
187 | }
188 | Text(
189 | text = text,
190 | textAlign = TextAlign.Center,
191 | style = MaterialTheme.typography.body1,
192 | color = MaterialTheme.colors.onBackground,
193 | letterSpacing = TextUnit(0F, TextUnitType.Sp)
194 | )
195 | }
196 | }
197 | }
198 |
199 | @Composable
200 | private fun PropertySection(imageDetail: UnsplashPhoto) {
201 | Row(
202 | modifier = Modifier
203 | .fillMaxWidth()
204 | .padding(top = 16.dp),
205 | horizontalArrangement = Arrangement.SpaceEvenly
206 | ) {
207 | SinglePropertyItem(value = imageDetail.views, title = "Views")
208 | SinglePropertyItem(value = imageDetail.downloads, title = "Downloads")
209 | SinglePropertyItem(value = imageDetail.likes, title = "Likes")
210 | }
211 | }
212 |
213 | @Composable
214 | private fun ExifInfoContainer(imageDetail: PhotoDetail) {
215 | Column(
216 | modifier = Modifier
217 | .fillMaxWidth()
218 | .padding(horizontal = 16.dp, vertical = 8.dp),
219 | verticalArrangement = Arrangement.spacedBy(24.dp)
220 | ) {
221 | Row(
222 | modifier = Modifier.fillMaxWidth(),
223 | horizontalArrangement = Arrangement.spacedBy(48.dp)
224 | ) {
225 | SingleExifItem(icon = Icons.Default.Screenshot, title = imageDetail.resolution)
226 | SingleExifItem(icon = Icons.Default.Dock, title = imageDetail.aperture)
227 | }
228 | Row(
229 | modifier = Modifier.fillMaxWidth(),
230 | horizontalArrangement = Arrangement.spacedBy(48.dp)
231 | ) {
232 | SingleExifItem(icon = Icons.Default.Camera, title = imageDetail.camera)
233 | SingleExifItem(icon = Icons.Default.Exposure, title = imageDetail.exposureTime)
234 | }
235 | Row(
236 | modifier = Modifier.fillMaxWidth(),
237 | horizontalArrangement = Arrangement.spacedBy(48.dp)
238 | ) {
239 | SingleExifItem(icon = Icons.Default.Dock, title = imageDetail.focalLength)
240 | SingleExifItem(icon = Icons.Default.Iso, title = imageDetail.iso)
241 | }
242 | }
243 | }
244 |
245 | @Composable
246 | private fun SingleExifItem(icon: ImageVector, title: String) {
247 | Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
248 | Image(imageVector = icon, contentDescription = icon.name)
249 | Text(text = title)
250 | }
251 | }
252 |
253 | @Composable
254 | private fun SinglePropertyItem(value: Int, title: String) {
255 | Column(
256 | modifier = Modifier
257 | .clip(RoundedCornerShape(10.dp))
258 | .background(MaterialTheme.colors.surface)
259 | .padding(vertical = 12.dp, horizontal = 24.dp),
260 | horizontalAlignment = Alignment.CenterHorizontally
261 | ) {
262 | Text(
263 | text = value.parseCount(),
264 | style = MaterialTheme.typography.body1,
265 | color = MaterialTheme.colors.onBackground
266 | )
267 | Text(
268 | text = title,
269 | style = MaterialTheme.typography.subtitle2,
270 | color = MaterialTheme.colors.onBackground
271 | )
272 | }
273 | }
274 |
--------------------------------------------------------------------------------