├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── shared ├── data │ ├── src │ │ ├── commonMain │ │ │ ├── sqldelight │ │ │ │ └── io │ │ │ │ │ └── github │ │ │ │ │ └── andremion │ │ │ │ │ └── jobster │ │ │ │ │ └── data │ │ │ │ │ └── local │ │ │ │ │ └── db │ │ │ │ │ ├── 1.sqm │ │ │ │ │ ├── Content.sq │ │ │ │ │ ├── JobContent.sq │ │ │ │ │ └── Job.sq │ │ │ └── kotlin │ │ │ │ └── io │ │ │ │ └── github │ │ │ │ └── andremion │ │ │ │ └── jobster │ │ │ │ └── data │ │ │ │ ├── di │ │ │ │ ├── InternalDataModule.kt │ │ │ │ └── DataModule.kt │ │ │ │ ├── remote │ │ │ │ ├── JobPostingSearcher.kt │ │ │ │ ├── model │ │ │ │ │ └── GetJobPostingResponse.kt │ │ │ │ ├── mapper │ │ │ │ │ └── GeminiApiMapper.kt │ │ │ │ ├── WebScrapper.kt │ │ │ │ └── api │ │ │ │ │ └── GeminiApi.kt │ │ │ │ ├── local │ │ │ │ └── db │ │ │ │ │ ├── DestructiveMigrationSchema.kt │ │ │ │ │ ├── mapper │ │ │ │ │ └── DatabaseMapper.kt │ │ │ │ │ └── JobDao.kt │ │ │ │ └── JobRepositoryImpl.kt │ │ ├── iosMain │ │ │ └── kotlin │ │ │ │ └── io │ │ │ │ └── github │ │ │ │ └── andremion │ │ │ │ └── jobster │ │ │ │ └── data │ │ │ │ └── di │ │ │ │ └── InternalDataModule.ios.kt │ │ └── androidMain │ │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── andremion │ │ │ └── jobster │ │ │ └── data │ │ │ ├── di │ │ │ └── InternalDataModule.android.kt │ │ │ └── remote │ │ │ └── api │ │ │ └── GeminiApiImpl.kt │ └── build.gradle.kts ├── ui │ ├── src │ │ ├── commonMain │ │ │ ├── composeResources │ │ │ │ ├── drawable │ │ │ │ │ ├── bg_gemini.png │ │ │ │ │ └── ic_gemini.xml │ │ │ │ └── values │ │ │ │ │ └── strings.xml │ │ │ └── kotlin │ │ │ │ └── io │ │ │ │ └── github │ │ │ │ └── andremion │ │ │ │ └── jobster │ │ │ │ ├── Jobster.kt │ │ │ │ ├── ui │ │ │ │ ├── navigation │ │ │ │ │ ├── PreCompose.kt │ │ │ │ │ ├── NavExtensions.kt │ │ │ │ │ ├── MainNavHost.kt │ │ │ │ │ └── HomeNavHost.kt │ │ │ │ ├── theme │ │ │ │ │ ├── Theme.kt │ │ │ │ │ └── Typography.kt │ │ │ │ ├── component │ │ │ │ │ └── BoxWithBackground.kt │ │ │ │ ├── animation │ │ │ │ │ ├── Transition.kt │ │ │ │ │ └── Lottie.kt │ │ │ │ ├── contentlist │ │ │ │ │ ├── ContentListScreen.kt │ │ │ │ │ └── ContentItem.kt │ │ │ │ ├── jobdetails │ │ │ │ │ └── JobDetailsScreen.kt │ │ │ │ └── joblist │ │ │ │ │ └── JobListScreen.kt │ │ │ │ └── di │ │ │ │ └── DI.kt │ │ ├── androidMain │ │ │ ├── AndroidManifest.xml │ │ │ └── kotlin │ │ │ │ └── io │ │ │ │ └── github │ │ │ │ └── andremion │ │ │ │ └── jobster │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MainApp.kt │ │ │ │ └── ui │ │ │ │ └── navigation │ │ │ │ └── NavExtensions.android.kt │ │ └── iosMain │ │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── andremion │ │ │ └── jobster │ │ │ ├── MainViewController.kt │ │ │ ├── di │ │ │ └── DI.kt │ │ │ └── ui │ │ │ └── navigation │ │ │ └── NavExtensions.ios.kt │ └── build.gradle.kts ├── domain │ ├── build.gradle.kts │ └── src │ │ └── commonMain │ │ └── kotlin │ │ └── io │ │ └── github │ │ └── andremion │ │ └── jobster │ │ └── domain │ │ ├── entity │ │ ├── SearchResult.kt │ │ ├── JobPosting.kt │ │ └── Job.kt │ │ ├── exception │ │ └── JobPostingSearchException.kt │ │ └── JobRepository.kt └── presentation │ ├── build.gradle.kts │ └── src │ └── commonMain │ └── kotlin │ └── io │ └── github │ └── andremion │ └── jobster │ └── presentation │ ├── joblist │ ├── JobListUiEvent.kt │ ├── JobListUiEffect.kt │ ├── JobListUiState.kt │ ├── di │ │ └── JobListPresentationModule.kt │ ├── mapper │ │ └── JobListMapper.kt │ └── JobListViewModel.kt │ ├── jobdetails │ ├── JobDetailsUiEffect.kt │ ├── JobDetailsUiState.kt │ ├── JobDetailsUiEvent.kt │ ├── di │ │ └── JobDetailsPresentationModule.kt │ └── JobDetailsViewModel.kt │ ├── contentlist │ ├── ContentListUiEffect.kt │ ├── ContentListUiEvent.kt │ ├── ContentListUiState.kt │ ├── di │ │ └── ContentListPresentationModule.kt │ └── ContentListViewModel.kt │ ├── jobpostingsearch │ ├── JobPostingSearchUiEffect.kt │ ├── di │ │ └── JobPostingSearchPresentationModule.kt │ ├── JobPostingSearchUiEvent.kt │ ├── JobPostingSearchUiState.kt │ ├── mapper │ │ └── JobSearchMapper.kt │ └── JobPostingSearchViewModel.kt │ ├── home │ ├── HomeUiEffect.kt │ ├── HomeUiState.kt │ ├── di │ │ └── HomePresentationModule.kt │ ├── HomeUiEvent.kt │ └── HomeViewModel.kt │ ├── di │ └── PresentationModule.kt │ └── AbsViewModel.kt ├── iosApp ├── iosApp │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── 1024.png │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── ui │ │ ├── ContentView.swift │ │ └── ComposeView.swift │ ├── data │ │ ├── GeminiInfo.plist │ │ ├── GeminiApiKey.swift │ │ └── GeminiApiImpl.swift │ └── iosAppApp.swift ├── iosApp.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcuserdata │ │ │ └── andrerego.xcuserdatad │ │ │ │ └── UserInterfaceState.xcuserstate │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ └── andrerego.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── iosAppUITests │ ├── iosAppUITestsLaunchTests.swift │ └── iosAppUITests.swift └── iosAppTests │ └── iosAppTests.swift ├── androidApp ├── src │ ├── main │ │ ├── ic_launcher-playstore.png │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── themes.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ └── ic_launcher.xml │ │ │ ├── xml │ │ │ │ ├── backup_rules.xml │ │ │ │ └── data_extraction_rules.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_foreground.xml │ │ └── AndroidManifest.xml │ └── debug │ │ └── res │ │ └── values │ │ └── strings.xml ├── proguard-rules.pro └── build.gradle.kts ├── .gitignore ├── settings.gradle.kts ├── .github └── workflows │ └── ci.yml ├── gradle.properties ├── gradlew.bat ├── README.md ├── gradlew └── docs └── privacy.html /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Jobster/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /shared/data/src/commonMain/sqldelight/io/github/andremion/jobster/data/local/db/1.sqm: -------------------------------------------------------------------------------- 1 | -- Handled by DestructiveMigrationSchema -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /androidApp/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Jobster/HEAD/androidApp/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /androidApp/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Jobster 3 | 4 | -------------------------------------------------------------------------------- /androidApp/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Jobster debug 3 | 4 | -------------------------------------------------------------------------------- /iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Jobster/HEAD/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /androidApp/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFAAC7FF 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/** 5 | .DS_Store 6 | **/build/ 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /shared/ui/src/commonMain/composeResources/drawable/bg_gemini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Jobster/HEAD/shared/ui/src/commonMain/composeResources/drawable/bg_gemini.png -------------------------------------------------------------------------------- /androidApp/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | Privacy Policy 11 |

12 | André Luiz Oliveira Rêgo built the Jobster app as 13 | a Free app. This SERVICE is provided by 14 | André Luiz Oliveira Rêgo at no cost and is intended for use as 15 | is. 16 |

17 |

18 | This page is used to inform visitors regarding my 19 | policies with the collection, use, and disclosure of Personal 20 | Information if anyone decided to use my Service. 21 |

22 |

23 | If you choose to use my Service, then you agree to 24 | the collection and use of information in relation to this 25 | policy. The Personal Information that I collect is 26 | used for providing and improving the Service. I will not use or share your information with 27 | anyone except as described in this Privacy Policy. 28 |

29 |

30 | The terms used in this Privacy Policy have the same meanings 31 | as in our Terms and Conditions, which are accessible at 32 | Jobster unless otherwise defined in this Privacy Policy. 33 |

34 |

Information Collection and Use

35 |

36 | For a better experience, while using our Service, I 37 | may require you to provide us with certain personally 38 | identifiable information. The information that 39 | I request will be retained on your device and is not collected by me in any way. 40 |

41 |

42 | The app does use third-party services that may collect 43 | information used to identify you. 44 |

45 |

46 | Link to the privacy policy of third-party service providers used 47 | by the app 48 |

49 | 53 |
54 |

Log Data

55 |

56 | I want to inform you that whenever you 57 | use my Service, in a case of an error in the app 58 | I collect data and information (through third-party 59 | products) on your phone called Log Data. This Log Data may 60 | include information such as your device Internet Protocol 61 | (“IP”) address, device name, operating system version, the 62 | configuration of the app when utilizing my Service, 63 | the time and date of your use of the Service, and other 64 | statistics. 65 |

66 |

Cookies

67 |

68 | Cookies are files with a small amount of data that are 69 | commonly used as anonymous unique identifiers. These are sent 70 | to your browser from the websites that you visit and are 71 | stored on your device's internal memory. 72 |

73 |

74 | This Service does not use these “cookies” explicitly. However, 75 | the app may use third-party code and libraries that use 76 | “cookies” to collect information and improve their services. 77 | You have the option to either accept or refuse these cookies 78 | and know when a cookie is being sent to your device. If you 79 | choose to refuse our cookies, you may not be able to use some 80 | portions of this Service. 81 |

82 |

Service Providers

83 |

84 | I may employ third-party companies and 85 | individuals due to the following reasons: 86 |

87 | 93 |

94 | I want to inform users of this Service 95 | that these third parties have access to their Personal 96 | Information. The reason is to perform the tasks assigned to 97 | them on our behalf. However, they are obligated not to 98 | disclose or use the information for any other purpose. 99 |

100 |

Security

101 |

102 | I value your trust in providing us your 103 | Personal Information, thus we are striving to use commercially 104 | acceptable means of protecting it. But remember that no method 105 | of transmission over the internet, or method of electronic 106 | storage is 100% secure and reliable, and I cannot 107 | guarantee its absolute security. 108 |

109 |

Links to Other Sites

110 |

111 | This Service may contain links to other sites. If you click on 112 | a third-party link, you will be directed to that site. Note 113 | that these external sites are not operated by me. 114 | Therefore, I strongly advise you to review the 115 | Privacy Policy of these websites. I have 116 | no control over and assume no responsibility for the content, 117 | privacy policies, or practices of any third-party sites or 118 | services. 119 |

120 |

Children’s Privacy

121 |

122 | These Services do not address anyone under the age of 13. 123 | I do not knowingly collect personally 124 | identifiable information from children under 13 years of age. In the case 125 | I discover that a child under 13 has provided 126 | me with personal information, I immediately 127 | delete this from our servers. If you are a parent or guardian 128 | and you are aware that your child has provided us with 129 | personal information, please contact me so that 130 | I will be able to do the necessary actions. 131 |

Changes to This Privacy Policy

132 |

133 | I may update our Privacy Policy from 134 | time to time. Thus, you are advised to review this page 135 | periodically for any changes. I will 136 | notify you of any changes by posting the new Privacy Policy on 137 | this page. 138 |

139 |

This policy is effective as of 2024-01-18

140 |

Contact Us

141 |

142 | If you have any questions or suggestions about my 143 | Privacy Policy, do not hesitate to contact me at andremion@gmail.com. 144 |

145 |

This privacy policy page was created at privacypolicytemplate.net and 147 | modified/generated by App Privacy Policy Generator

149 | 150 | 151 | -------------------------------------------------------------------------------- /shared/presentation/src/commonMain/kotlin/io/github/andremion/jobster/presentation/jobpostingsearch/JobPostingSearchViewModel.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024. André Luiz Oliveira Rêgo 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.github.andremion.jobster.presentation.jobpostingsearch 18 | 19 | import io.github.andremion.jobster.domain.JobRepository 20 | import io.github.andremion.jobster.domain.entity.Job 21 | import io.github.andremion.jobster.domain.entity.JobPosting 22 | import io.github.andremion.jobster.domain.exception.JobPostingSearchException 23 | import io.github.andremion.jobster.presentation.AbsViewModel 24 | import io.github.andremion.jobster.presentation.WhileSubscribed 25 | import io.github.andremion.jobster.presentation.jobpostingsearch.mapper.transform 26 | import kotlinx.coroutines.flow.MutableStateFlow 27 | import kotlinx.coroutines.flow.StateFlow 28 | import kotlinx.coroutines.flow.firstOrNull 29 | import kotlinx.coroutines.flow.stateIn 30 | import kotlinx.coroutines.flow.update 31 | import kotlinx.coroutines.launch 32 | import moe.tlaster.precompose.viewmodel.viewModelScope 33 | 34 | class JobPostingSearchViewModel( 35 | private val jobRepository: JobRepository, 36 | ) : AbsViewModel() { 37 | 38 | private var searchJob: kotlinx.coroutines.Job? = null 39 | 40 | private val mutableUiState = MutableStateFlow(JobPostingSearchUiState()) 41 | 42 | override val uiState: StateFlow = mutableUiState 43 | .stateIn( 44 | scope = viewModelScope, 45 | started = WhileSubscribed, 46 | initialValue = JobPostingSearchUiState(isSearchBarActive = true) 47 | ) 48 | 49 | override fun onUiEvent(uiEvent: JobPostingSearchUiEvent) { 50 | when (uiEvent) { 51 | is JobPostingSearchUiEvent.BackClick -> { 52 | mutableUiEffect.tryEmit(JobPostingSearchUiEffect.NavigateBack) 53 | } 54 | 55 | is JobPostingSearchUiEvent.UpdateUrl -> { 56 | searchJob?.cancel() 57 | mutableUiState.update { uiState -> 58 | uiState.copy( 59 | url = uiEvent.url, 60 | isLoading = false, 61 | error = null, 62 | ) 63 | } 64 | } 65 | 66 | is JobPostingSearchUiEvent.UpdateSearchBarActive -> { 67 | searchJob?.cancel() 68 | mutableUiState.update { uiState -> 69 | uiState.copy( 70 | isSearchBarActive = uiEvent.isActive, 71 | isLoading = false, 72 | error = null, 73 | ) 74 | } 75 | } 76 | 77 | is JobPostingSearchUiEvent.SearchBarBackClick -> { 78 | searchJob?.cancel() 79 | mutableUiState.update { uiState -> 80 | uiState.copy( 81 | isSearchBarActive = false, 82 | isLoading = false, 83 | error = null, 84 | ) 85 | } 86 | } 87 | 88 | is JobPostingSearchUiEvent.SearchBarClearClick -> { 89 | searchJob?.cancel() 90 | mutableUiState.update { uiState -> 91 | uiState.copy( 92 | url = "", 93 | isLoading = false, 94 | error = null, 95 | ) 96 | } 97 | } 98 | 99 | is JobPostingSearchUiEvent.SearchClick -> { 100 | val url = uiState.value.url 101 | if (url.isNotBlank()) { 102 | mutableUiState.update { uiState -> 103 | uiState.copy( 104 | isLoading = true, 105 | jobPosting = null, 106 | error = null, 107 | ) 108 | } 109 | searchJob?.cancel() 110 | searchJob = viewModelScope.launch { 111 | runCatching { 112 | val jobPosting = jobRepository.searchJobPosting(url) 113 | val contentIds = jobPosting.contents.map(JobPosting.Content::url) 114 | val existingContentIds = jobRepository.getContentsByIds(contentIds) 115 | .firstOrNull()?.map(Job.Content::id) ?: emptyList() 116 | jobPosting.transform(existingContentIds) 117 | }.onSuccess { jobPosting -> 118 | mutableUiState.update { uiState -> 119 | uiState.copy( 120 | isSearchBarActive = false, 121 | isLoading = false, 122 | jobPosting = jobPosting, 123 | ) 124 | } 125 | }.onFailure { cause -> 126 | cause.printStackTrace() 127 | mutableUiState.update { uiState -> 128 | uiState.copy( 129 | isLoading = false, 130 | error = cause as? JobPostingSearchException, 131 | ) 132 | } 133 | } 134 | } 135 | } 136 | } 137 | 138 | is JobPostingSearchUiEvent.ContentTitleClick -> { 139 | mutableUiEffect.tryEmit(JobPostingSearchUiEffect.NavigateToUrl(uiEvent.url)) 140 | } 141 | 142 | is JobPostingSearchUiEvent.ContentSwitchClick -> { 143 | viewModelScope.launch { 144 | val state = uiState.value 145 | val jobPosting = requireNotNull(state.jobPosting) 146 | if (uiEvent.isChecked) { 147 | jobRepository.save( 148 | jobPosting = jobPosting.transform(state.url), 149 | contents = listOf(uiEvent.content.transform()) 150 | ) 151 | } else { 152 | jobRepository.delete( 153 | jobId = state.url, 154 | contentId = uiEvent.content.url 155 | ) 156 | } 157 | mutableUiState.update { uiState -> 158 | uiState.copy( 159 | jobPosting = jobPosting.copy( 160 | contents = jobPosting.contents.map { content -> 161 | if (content.url == uiEvent.content.url) { 162 | content.copy(isChecked = uiEvent.isChecked) 163 | } else { 164 | content 165 | } 166 | } 167 | ) 168 | ) 169 | } 170 | } 171 | } 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /androidApp/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 16 | 19 | 24 | 27 | 30 | 35 | 38 | 41 | 46 | 49 | 54 | 59 | 62 | 65 | 68 | 71 | 74 | 77 | 80 | 81 | 82 | --------------------------------------------------------------------------------